tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI-Logo

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5<a href="https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"><img src="https://github.com/Tim55667757/TKSBrokerAPI/blob/develop/docs/media/TKSBrokerAPI-Logo.png?raw=true" alt="TKSBrokerAPI-Logo" width="780" target="_blank" /></a>
   6
   7**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   8as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   9from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
  10
  11TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  12the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  13
  14- **Open account for trading:** https://tinkoff.ru/sl/AaX1Et1omnH
  15- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  16- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  17- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  18- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  19- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  20"""
  21
  22# Copyright (c) 2022 Gilmillin Timur Mansurovich
  23#
  24# Licensed under the Apache License, Version 2.0 (the "License");
  25# you may not use this file except in compliance with the License.
  26# You may obtain a copy of the License at
  27#
  28#     http://www.apache.org/licenses/LICENSE-2.0
  29#
  30# Unless required by applicable law or agreed to in writing, software
  31# distributed under the License is distributed on an "AS IS" BASIS,
  32# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  33# See the License for the specific language governing permissions and
  34# limitations under the License.
  35
  36
  37import sys
  38import os
  39from argparse import ArgumentParser
  40from importlib.metadata import version
  41
  42from dateutil.tz import tzlocal
  43from time import sleep
  44
  45import re
  46import json
  47import requests
  48import traceback as tb
  49from typing import Union
  50
  51from multiprocessing import cpu_count, Lock
  52from multiprocessing.pool import ThreadPool
  53import pandas as pd
  54
  55from mako.template import Template  # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
  56from Templates import *  # Some html-templates used by reporting methods in TKSBrokerAPI module
  57from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  58from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  59
  60from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator)
  61from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  62
  63import UniLogger as uLog  # Logger for TKSBrokerAPI
  64
  65
  66# --- Common technical parameters:
  67
  68PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  69uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  70uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  71uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  72
  73__version__ = "1.6"  # The "major.minor" version setup here, but build number define at the build-server only
  74
  75CPU_COUNT = cpu_count()  # host's real CPU count
  76CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  77
  78
  79class TinkoffBrokerServer:
  80    """
  81    This class implements methods to work with Tinkoff broker server.
  82
  83    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  84
  85    About `token`: https://tinkoff.github.io/investAPI/token/
  86    """
  87    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  88        """
  89        Main class init.
  90
  91        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  92        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  93                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  94        :param useCache: use default cache file with raw data to use instead of `iList`.
  95                         True by default. Cache is auto-update if new day has come.
  96                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  97        :param defaultCache: path to default cache file. `dump.json` by default.
  98        """
  99        if token is None or not token:
 100            try:
 101                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 102                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 103
 104            except KeyError:
 105                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 106                raise Exception("Token required")
 107
 108        else:
 109            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 110            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 111
 112        if accountId is None or not accountId:
 113            try:
 114                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 115                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 116
 117            except KeyError:
 118                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 119
 120        else:
 121            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 122            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 123
 124        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 125        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 126
 127        Latest version: https://pypi.org/project/tksbrokerapi/
 128        """
 129
 130        self._tag = ""
 131        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 132
 133        self.__lock = Lock()  # initialize multiprocessing mutex lock
 134
 135        self._precision = 4  # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file
 136
 137        self.aliases = TKS_TICKER_ALIASES
 138        """Some aliases instead official tickers.
 139
 140        See also: `TKSEnums.TKS_TICKER_ALIASES`
 141        """
 142
 143        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 144
 145        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 146
 147        self._ticker = ""
 148        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 149
 150        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 151        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 152
 153        See also: `SearchByTicker()`, `SearchInstruments()`.
 154        """
 155
 156        self._figi = ""
 157        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 158
 159        See also: `SearchByFIGI()`, `SearchInstruments()`.
 160        """
 161
 162        self.depth = 1
 163        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 164
 165        See also: `GetCurrentPrices()`.
 166        """
 167
 168        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 169        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 170
 171        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 172        """
 173
 174        uLogger.debug("Broker API server: {}".format(self.server))
 175
 176        self.timeout = 15
 177        """Server operations timeout in seconds. Default: `15`.
 178
 179        See also: `SendAPIRequest()`.
 180        """
 181
 182        self.headers = {
 183            "Content-Type": "application/json",
 184            "accept": "application/json",
 185            "Authorization": "Bearer {}".format(self.token),
 186            "x-app-name": "Tim55667757.TKSBrokerAPI",
 187        }
 188        """
 189        Headers which send in every request to broker server. Please, do not change it!
 190        Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`.
 191
 192        See also: `SendAPIRequest()`.
 193        """
 194
 195        self.body = None
 196        """Request body which send to broker server. Default: `None`.
 197
 198        See also: `SendAPIRequest()`.
 199        """
 200
 201        self.moreDebug = False
 202        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 203
 204        self.useHTMLReports = False
 205        """
 206        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 207        
 208        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 209        """
 210
 211        self.historyFile = None
 212        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 213
 214        See also: `History()`.
 215        """
 216
 217        self.htmlHistoryFile = "index.html"
 218        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 219
 220        See also: `ShowHistoryChart()`.
 221        """
 222
 223        self.instrumentsFile = "instruments.md"
 224        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 225
 226        See also: `ShowInstrumentsInfo()`.
 227        """
 228
 229        self.searchResultsFile = "search-results.md"
 230        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 231
 232        See also: `SearchInstruments()`.
 233        """
 234
 235        self.pricesFile = "prices.md"
 236        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 237
 238        See also: `GetListOfPrices()`.
 239        """
 240
 241        self.infoFile = "info.md"
 242        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 243
 244        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 245        """
 246
 247        self.bondsXLSXFile = "ext-bonds.xlsx"
 248        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 249        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 250
 251        See also: `ExtendBondsData()`.
 252        """
 253
 254        self.calendarFile = "calendar.md"
 255        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 256        
 257        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 258
 259        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 260        """
 261
 262        self.overviewFile = "overview.md"
 263        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 264
 265        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 266        """
 267
 268        self.overviewDigestFile = "overview-digest.md"
 269        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 270
 271        See also: `Overview()` with parameter `details="digest"`.
 272        """
 273
 274        self.overviewPositionsFile = "overview-positions.md"
 275        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 276
 277        See also: `Overview()` with parameter `details="positions"`.
 278        """
 279
 280        self.overviewOrdersFile = "overview-orders.md"
 281        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 282
 283        See also: `Overview()` with parameter `details="orders"`.
 284        """
 285
 286        self.overviewAnalyticsFile = "overview-analytics.md"
 287        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 288
 289        See also: `Overview()` with parameter `details="analytics"`.
 290        """
 291
 292        self.overviewBondsCalendarFile = "overview-calendar.md"
 293        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 294
 295        See also: `Overview()` with parameter `details="calendar"`.
 296        """
 297
 298        self.reportFile = "deals.md"
 299        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 300
 301        See also: `Deals()`.
 302        """
 303
 304        self.withdrawalLimitsFile = "limits.md"
 305        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 306
 307        See also: `OverviewLimits()` and `RequestLimits()`.
 308        """
 309
 310        self.userInfoFile = "user-info.md"
 311        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 312
 313        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 314        """
 315
 316        self.userAccountsFile = "accounts.md"
 317        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 318
 319        See also: `OverviewAccounts()`, `RequestAccounts()`.
 320        """
 321
 322        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 323        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 324
 325        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 326
 327        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 328        """
 329
 330        self.iList = None  # init iList for raw instruments data
 331        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 332        
 333        See also: `Listing()`, `DumpInstruments()`.
 334        """
 335
 336        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 337        if useCache:
 338            if os.path.exists(self.iListDumpFile):
 339                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 340                curTime = datetime.now(tzutc())
 341
 342                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 343                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 344
 345                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 346
 347                else:
 348                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 349
 350                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 351                        os.path.abspath(self.iListDumpFile),
 352                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 353                    ))
 354
 355            else:
 356                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 357                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 358
 359        else:
 360            self.iList = self.Listing()  # request new raw instruments data from broker server
 361            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 362
 363        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 364        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 365
 366        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 367        """
 368
 369    @property
 370    def tag(self) -> str:
 371        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 372        return self._tag
 373
 374    @tag.setter
 375    def tag(self, value):
 376        """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 377        self._tag = str(value)
 378
 379        if self._tag:
 380            for handler in uLogger.handlers:
 381                handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag)))
 382
 383            uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag))
 384
 385        else:
 386            for handler in uLogger.handlers:
 387                handler.setFormatter(uLog.logging.Formatter(uLog.formatString))
 388
 389            uLogger.debug("Default logger format is used")
 390
 391    @property
 392    def ticker(self) -> str:
 393        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 394
 395        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 396        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 397
 398        See also: `SearchByTicker()`, `SearchInstruments()`.
 399        """
 400        return self._ticker
 401
 402    @ticker.setter
 403    def ticker(self, value):
 404        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 405
 406        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 407        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 408
 409        See also: `SearchByTicker()`, `SearchInstruments()`.
 410        """
 411        self._ticker = str(value).upper()  # Tickers may be upper case only
 412
 413    @property
 414    def figi(self) -> str:
 415        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 416
 417        See also: `SearchByFIGI()`, `SearchInstruments()`.
 418        """
 419        return self._figi
 420
 421    @figi.setter
 422    def figi(self, value):
 423        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 424
 425        See also: `SearchByFIGI()`, `SearchInstruments()`.
 426        """
 427        self._figi = str(value).upper()  # FIGI may be upper case only
 428
 429    def _ParseJSON(self, rawData="{}") -> dict:
 430        """
 431        Parse JSON from response string.
 432
 433        :param rawData: this is a string with JSON-formatted text.
 434        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 435        """
 436        try:
 437            responseJSON = json.loads(rawData) if rawData else {}
 438
 439            if self.moreDebug:
 440                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 441
 442            return responseJSON
 443
 444        except Exception as e:
 445            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 446
 447            return {}
 448
 449    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 450        """
 451        Send GET or POST request to broker server and receive JSON object.
 452
 453        self.header: must be defining with dictionary of headers.
 454        self.body: if define then used as request body. None by default.
 455        self.timeout: global request timeout, 15 seconds by default.
 456        :param url: url with REST request.
 457        :param reqType: send "GET" or "POST" request. "GET" by default.
 458        :param retry: how many times retry after first request if an 5xx server errors occurred.
 459        :param pause: sleep time in seconds between retries.
 460        :return: response JSON (dictionary) from broker.
 461        """
 462        if reqType.upper() not in ("GET", "POST"):
 463            uLogger.error("You can define request type: `GET` or `POST`!")
 464            raise Exception("Incorrect value")
 465
 466        if self.moreDebug:
 467            uLogger.debug("Request parameters:")
 468            uLogger.debug("    - REST API URL: {}".format(url))
 469            uLogger.debug("    - request type: {}".format(reqType))
 470            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 471            uLogger.debug("    - body:\n{}".format(self.body))
 472
 473        # fast hack to avoid all operations with some tickers/FIGI
 474        responseJSON = {}
 475        oK = True
 476        for item in self.exclude:
 477            if item in url:
 478                if self.moreDebug:
 479                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 480
 481                oK = False
 482                break
 483
 484        if oK:
 485            with self.__lock:  # acquire the mutex lock
 486                counter = 0
 487                response = None
 488                errMsg = ""
 489
 490                while not response and counter <= retry:
 491                    if reqType == "GET":
 492                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 493
 494                    if reqType == "POST":
 495                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 496
 497                    if self.moreDebug:
 498                        uLogger.debug("Response:")
 499                        uLogger.debug("    - status code: {}".format(response.status_code))
 500                        uLogger.debug("    - reason: {}".format(response.reason))
 501                        uLogger.debug("    - body length: {}".format(len(response.text)))
 502                        uLogger.debug("    - headers:\n{}".format(response.headers))
 503
 504                    # Server returns some headers:
 505                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 506                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 507                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 508                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 509                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 510                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 511                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 512                        sleep(rateLimitWait)
 513
 514                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 515                    if 400 <= response.status_code < 500:
 516                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 517                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 518
 519                        if "code" in response.text and "message" in response.text:
 520                            msgDict = self._ParseJSON(rawData=response.text)
 521                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 522
 523                        counter = retry + 1  # do not retry for 4xx errors
 524
 525                    if 500 <= response.status_code < 600:
 526                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 527                        uLogger.debug("    - not oK, {}".format(errMsg))
 528
 529                        if "code" in response.text and "message" in response.text:
 530                            errMsgDict = self._ParseJSON(rawData=response.text)
 531                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 532
 533                        counter += 1
 534
 535                        if counter <= retry:
 536                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 537                            sleep(pause)
 538
 539                responseJSON = self._ParseJSON(rawData=response.text)
 540
 541                if errMsg:
 542                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 543                    uLogger.error("    - not oK, {}".format(errMsg))
 544
 545        return responseJSON
 546
 547    def _IUpdater(self, iType: str) -> tuple:
 548        """
 549        Request instrument by type from server. See available API methods for instruments:
 550        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 551        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 552        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 553        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 554        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 555
 556        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 557        :return: tuple with iType name and list of available instruments of current type for defined user token.
 558        """
 559        result = []
 560
 561        if iType in TKS_INSTRUMENTS:
 562            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 563
 564            # all instruments have the same body in API v2 requests:
 565            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 566            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 567            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 568
 569        return iType, result
 570
 571    def _IWrapper(self, kwargs):
 572        """
 573        Wrapper runs instrument's update method `_IUpdater()`.
 574        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 575        """
 576        return self._IUpdater(**kwargs)
 577
 578    def Listing(self) -> dict:
 579        """
 580        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 581
 582        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 583        """
 584        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 585        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 586
 587        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 588        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 589        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 590
 591        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 592        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 593        poolUpdater.close()  # close the thread pool
 594        poolUpdater.join()  # wait a moment until all data returns from threads
 595
 596        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 597        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 598        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 599
 600        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 601        for iType in iList.keys():
 602            for ticker in iList[iType]:
 603                iList[iType][ticker]["type"] = iType
 604
 605                if "minPriceIncrement" in iList[iType][ticker].keys():
 606                    iList[iType][ticker]["step"] = NanoToFloat(
 607                        iList[iType][ticker]["minPriceIncrement"]["units"],
 608                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 609                    )
 610
 611                else:
 612                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 613
 614        return iList
 615
 616    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 617        """
 618        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 619
 620        See also: `DumpInstruments()`, `Listing()`.
 621
 622        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 623                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 624        """
 625        if self.iListDumpFile is None or not self.iListDumpFile:
 626            uLogger.error("Output name of dump file must be defined!")
 627            raise Exception("Filename required")
 628
 629        if not self.iList or forceUpdate:
 630            self.iList = self.Listing()
 631
 632        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 633
 634        # Save as XLSX with separated sheets for every type of instruments:
 635        with pd.ExcelWriter(
 636                path=xlsxDumpFile,
 637                date_format=TKS_DATE_FORMAT,
 638                datetime_format=TKS_DATE_TIME_FORMAT,
 639                mode="w",
 640        ) as writer:
 641            for iType in TKS_INSTRUMENTS:
 642                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 643                df = df[sorted(df)]  # sorted by column names
 644                df = df.applymap(
 645                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 646                    na_action="ignore",
 647                )  # converting numbers from nano-type to float in every cell
 648                df.to_excel(
 649                    writer,
 650                    sheet_name=iType,
 651                    encoding="UTF-8",
 652                    freeze_panes=(1, 1),
 653                )  # saving as XLSX-file with freeze first row and column as headers
 654
 655        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 656
 657    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 658        """
 659        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 660        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 661
 662        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 663
 664        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 665                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 666        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 667        """
 668        if self.iListDumpFile is None or not self.iListDumpFile:
 669            uLogger.error("Output name of dump file must be defined!")
 670            raise Exception("Filename required")
 671
 672        if not self.iList or forceUpdate:
 673            self.iList = self.Listing()
 674
 675        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 676        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 677            fH.write(jsonDump)
 678
 679        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 680
 681        return jsonDump
 682
 683    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
 684        """
 685        Show information about one instrument defined by json data and prints it in Markdown format.
 686
 687        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 688
 689        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 690        :param show: if `True` then also printing information about instrument and its current price.
 691        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
 692        :return: multilines text in Markdown format with information about one instrument.
 693        """
 694        splitLine = "|                                                             |                                                        |\n"
 695        infoText = ""
 696
 697        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 698            info = [
 699                "# Main information\n\n",
 700                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 701                "| Parameters                                                  | Values                                                 |\n",
 702                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 703                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 704                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 705            ]
 706
 707            if "sector" in iJSON.keys() and iJSON["sector"]:
 708                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 709
 710            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 711                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 712
 713            info.extend([
 714                splitLine,
 715                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 716                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 717            ])
 718
 719            if "isin" in iJSON.keys() and iJSON["isin"]:
 720                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 721
 722            if "classCode" in iJSON.keys():
 723                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 724
 725            info.extend([
 726                splitLine,
 727                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 728                splitLine,
 729                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 730                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 731                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 732            ])
 733
 734            if iJSON["figi"]:
 735                self._figi = iJSON["figi"]
 736                iJSON = iJSON | self.RequestTradingStatus()
 737
 738                info.extend([
 739                    splitLine,
 740                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 741                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 742                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 743                ])
 744
 745            info.append(splitLine)
 746
 747            if "type" in iJSON.keys() and iJSON["type"]:
 748                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 749
 750                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 751                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 752
 753            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 754                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 755
 756            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 757                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 758
 759            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 760                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 761
 762            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 763                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 764
 765            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 766                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 767
 768            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 769                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 770
 771            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 772                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 773
 774            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 775                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 776
 777            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 778                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 779
 780            if "currency" in iJSON.keys():
 781                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 782
 783            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 784                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 785
 786            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 787                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 788
 789            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 790                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 791
 792            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 793                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 794
 795            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 796                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 797
 798            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 799                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 800
 801            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 802                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 803
 804            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 805                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 806
 807            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 808                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 809
 810            iExt = None
 811            if iJSON["type"] == "Bonds":
 812                info.extend([
 813                    splitLine,
 814                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 815                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 816                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 817                        iJSON["nominal"]["currency"],
 818                    )),
 819                ])
 820
 821                if "floatingCouponFlag" in iJSON.keys():
 822                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 823
 824                if "amortizationFlag" in iJSON.keys():
 825                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 826
 827                info.append(splitLine)
 828
 829                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 830                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 831
 832                if iJSON["figi"]:
 833                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 834
 835                    info.extend([
 836                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 837                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 838                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 839                    ])
 840
 841                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 842                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 843                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 844                        iJSON["aciValue"]["currency"]
 845                    )))
 846
 847            if "currentPrice" in iJSON.keys():
 848                info.append(splitLine)
 849
 850                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 851                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 852
 853                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 854                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 855                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 856                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 857                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 858
 859                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 860                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 861
 862                info.extend([
 863                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 864                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 865                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 866                    )),
 867                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 868                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 869                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 870                    )),
 871                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 872                        "{:.2f}%{}".format(
 873                            iJSON["currentPrice"]["changes"],
 874                            " ({}{:.2f} {})".format(
 875                                "+" if bondChangesDelta > 0 else "",
 876                                bondChangesDelta,
 877                                aciCurrency
 878                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 879                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 880                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 881                                currency
 882                            ),
 883                        )
 884                    ),
 885                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 886                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 887                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 888                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 889                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 890                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 891                    )),
 892                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 893                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 894                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 895                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 896                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 897                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 898                    )),
 899                ])
 900
 901            if "lot" in iJSON.keys():
 902                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 903
 904            if "step" in iJSON.keys() and iJSON["step"] != 0:
 905                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 906
 907            # Add bond payment calendar:
 908            if iJSON["type"] == "Bonds":
 909                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 910                info.extend(["\n#", strCalendar])
 911
 912            infoText += "".join(info)
 913
 914            if show and not onlyFiles:
 915                uLogger.info("{}".format(infoText))
 916
 917            if self.infoFile is not None and (show or onlyFiles):
 918                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 919                    fH.write(infoText)
 920
 921                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 922
 923                if self.useHTMLReports:
 924                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 925                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 926                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 927
 928                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 929
 930        return infoText
 931
 932    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 933        """
 934        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 935
 936        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 937        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 938        :return: JSON formatted data with information about instrument.
 939        """
 940        tickerJSON = {}
 941        if self.moreDebug:
 942            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 943
 944        if not self._ticker:
 945            uLogger.warning("self._ticker variable is not be empty!")
 946
 947        else:
 948            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 949                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 950                raise Exception("Instrument not allowed")
 951
 952            if not self.iList:
 953                self.iList = self.Listing()
 954
 955            if self._ticker in self.iList["Shares"].keys():
 956                tickerJSON = self.iList["Shares"][self._ticker]
 957                if self.moreDebug:
 958                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 959
 960            elif self._ticker in self.iList["Currencies"].keys():
 961                tickerJSON = self.iList["Currencies"][self._ticker]
 962                if self.moreDebug:
 963                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 964
 965            elif self._ticker in self.iList["Bonds"].keys():
 966                tickerJSON = self.iList["Bonds"][self._ticker]
 967                if self.moreDebug:
 968                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 969
 970            elif self._ticker in self.iList["Etfs"].keys():
 971                tickerJSON = self.iList["Etfs"][self._ticker]
 972                if self.moreDebug:
 973                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 974
 975            elif self._ticker in self.iList["Futures"].keys():
 976                tickerJSON = self.iList["Futures"][self._ticker]
 977                if self.moreDebug:
 978                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 979
 980        if tickerJSON:
 981            self._figi = tickerJSON["figi"]
 982
 983            if requestPrice:
 984                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 985
 986                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 987                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 988
 989                else:
 990                    tickerJSON["currentPrice"]["changes"] = 0
 991
 992            if show:
 993                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 994
 995        else:
 996            if show:
 997                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 998
 999        return tickerJSON
1000
1001    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1002        """
1003        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1004
1005        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1006        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1007        :return: JSON formatted data with information about instrument.
1008        """
1009        figiJSON = {}
1010        if self.moreDebug:
1011            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
1012
1013        if not self._figi:
1014            uLogger.warning("self._figi variable is not be empty!")
1015
1016        else:
1017            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1018                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
1019                raise Exception("Instrument not allowed")
1020
1021            if not self.iList:
1022                self.iList = self.Listing()
1023
1024            for item in self.iList["Shares"].keys():
1025                if self._figi == self.iList["Shares"][item]["figi"]:
1026                    figiJSON = self.iList["Shares"][item]
1027
1028                    if self.moreDebug:
1029                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1030
1031                    break
1032
1033            if not figiJSON:
1034                for item in self.iList["Currencies"].keys():
1035                    if self._figi == self.iList["Currencies"][item]["figi"]:
1036                        figiJSON = self.iList["Currencies"][item]
1037
1038                        if self.moreDebug:
1039                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1040
1041                        break
1042
1043            if not figiJSON:
1044                for item in self.iList["Bonds"].keys():
1045                    if self._figi == self.iList["Bonds"][item]["figi"]:
1046                        figiJSON = self.iList["Bonds"][item]
1047
1048                        if self.moreDebug:
1049                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1050
1051                        break
1052
1053            if not figiJSON:
1054                for item in self.iList["Etfs"].keys():
1055                    if self._figi == self.iList["Etfs"][item]["figi"]:
1056                        figiJSON = self.iList["Etfs"][item]
1057
1058                        if self.moreDebug:
1059                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1060
1061                        break
1062
1063            if not figiJSON:
1064                for item in self.iList["Futures"].keys():
1065                    if self._figi == self.iList["Futures"][item]["figi"]:
1066                        figiJSON = self.iList["Futures"][item]
1067
1068                        if self.moreDebug:
1069                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1070
1071                        break
1072
1073        if figiJSON:
1074            self._figi = figiJSON["figi"]
1075            self._ticker = figiJSON["ticker"]
1076
1077            if requestPrice:
1078                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1079
1080                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1081                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1082
1083                else:
1084                    figiJSON["currentPrice"]["changes"] = 0
1085
1086            if show:
1087                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1088
1089        else:
1090            if show:
1091                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1092
1093        return figiJSON
1094
1095    def GetCurrentPrices(self, show: bool = True) -> dict:
1096        """
1097        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1098        `{"buy": [{"price": 1243.8, "quantity": 193},
1099                  {"price": 1244.0, "quantity": 168},
1100                  {"price": 1244.8, "quantity": 5},
1101                  {"price": 1245.0, "quantity": 61},
1102                  {"price": 1245.4, "quantity": 60}],
1103          "sell": [{"price": 1243.6, "quantity": 8},
1104                   {"price": 1242.6, "quantity": 10},
1105                   {"price": 1242.4, "quantity": 18},
1106                   {"price": 1242.2, "quantity": 50},
1107                   {"price": 1242.0, "quantity": 113}],
1108          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1109        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1110        - sell: list of dicts with Buyers prices,
1111            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1112            - quantity: volume value by current price in lots,
1113        - limitUp: current trade session limit price, maximum,
1114        - limitDown: current trade session limit price, minimum,
1115        - lastPrice: last deal price of the instrument,
1116        - closePrice: previous trade session close price of the instrument.
1117
1118        See also: `SearchByTicker()` and `SearchByFIGI()`.
1119        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1120        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1121
1122        :param show: if `True` then print DOM to log and console.
1123        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1124                 If an error occurred then returns an empty record:
1125                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1126        """
1127        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1128
1129        if self.depth < 1:
1130            uLogger.error("Depth of Market (DOM) must be >=1!")
1131            raise Exception("Incorrect value")
1132
1133        if not (self._ticker or self._figi):
1134            uLogger.error("self._ticker or self._figi variables must be defined!")
1135            raise Exception("Ticker or FIGI required")
1136
1137        if self._ticker and not self._figi:
1138            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1139            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1140
1141        if not self._ticker and self._figi:
1142            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1143            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1144
1145        if not self._figi:
1146            uLogger.error("FIGI is not defined!")
1147            raise Exception("Ticker or FIGI required")
1148
1149        else:
1150            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1151
1152            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1153            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1154            self.body = str({"figi": self._figi, "depth": self.depth})
1155            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1156
1157            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1158                # list of dicts with sellers orders:
1159                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1160
1161                # list of dicts with buyers orders:
1162                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1163
1164                # max price of instrument at this time:
1165                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1166
1167                # min price of instrument at this time:
1168                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1169
1170                # last price of deal with instrument:
1171                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1172
1173                # last close price of instrument:
1174                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1175
1176            else:
1177                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1178                uLogger.debug("Server response: {}".format(pricesResponse))
1179
1180            if show:
1181                if prices["buy"] or prices["sell"]:
1182                    info = [
1183                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1184                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1185                            self._ticker,
1186                            self._figi,
1187                            self.depth,
1188                        ),
1189                        "-" * 60, "\n",
1190                        "             Orders of Buyers | Orders of Sellers\n",
1191                        "-" * 60, "\n",
1192                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1193                        "-" * 60, "\n",
1194                    ]
1195
1196                    if not prices["buy"]:
1197                        info.append("                              | No orders!\n")
1198                        sumBuy = 0
1199
1200                    else:
1201                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1202                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1203                        for item in maxMinSorted:
1204                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1205
1206                    if not prices["sell"]:
1207                        info.append("No orders!                    |\n")
1208                        sumSell = 0
1209
1210                    else:
1211                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1212                        for item in prices["sell"]:
1213                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1214
1215                    info.extend([
1216                        "-" * 60, "\n",
1217                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1218                        "-" * 60, "\n",
1219                    ])
1220
1221                    infoText = "".join(info)
1222
1223                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1224
1225                else:
1226                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1227
1228        return prices
1229
1230    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1231        """
1232        This method get and show information about all available broker instruments for current user account.
1233        If `instrumentsFile` string is not empty then also save information to this file.
1234
1235        :param show: if `True` then print results to console, if `False` — print only to file.
1236        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1237        :return: multi-lines string with all available broker instruments.
1238        """
1239        if not self.iList:
1240            self.iList = self.Listing()
1241
1242        info = [
1243            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1244            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1245        ]
1246
1247        # add instruments count by type:
1248        for iType in self.iList.keys():
1249            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1250
1251        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1252        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1253
1254        # generating info tables with all instruments by type:
1255        for iType in self.iList.keys():
1256            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1257
1258            for instrument in self.iList[iType].keys():
1259                iName = self.iList[iType][instrument]["name"]  # instrument's name
1260                if len(iName) > 57:
1261                    iName = "{}...".format(iName[:54])  # right trim for a long string
1262
1263                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1264                    self.iList[iType][instrument]["ticker"],
1265                    iName,
1266                    self.iList[iType][instrument]["figi"],
1267                    self.iList[iType][instrument]["currency"],
1268                    self.iList[iType][instrument]["lot"],
1269                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1270                ))
1271
1272        infoText = "".join(info)
1273
1274        if show and not onlyFiles:
1275            uLogger.info(infoText)
1276
1277        if self.instrumentsFile and (show or onlyFiles):
1278            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1279                fH.write(infoText)
1280
1281            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1282
1283            if self.useHTMLReports:
1284                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1285                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1286                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1287
1288                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1289
1290        return infoText
1291
1292    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1293        """
1294        This method search and show information about instruments by part of its ticker, FIGI or name.
1295        If `searchResultsFile` string is not empty then also save information to this file.
1296
1297        :param pattern: string with part of ticker, FIGI or instrument's name.
1298        :param show: if `True` then print results to console, if `False` — return list of result only.
1299        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1300        :return: list of dictionaries with all found instruments.
1301        """
1302        if not self.iList:
1303            self.iList = self.Listing()
1304
1305        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1306        compiledPattern = re.compile(pattern, re.IGNORECASE)
1307
1308        for iType in self.iList:
1309            for instrument in self.iList[iType].values():
1310                searchResult = compiledPattern.search(" ".join(
1311                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1312                ))
1313
1314                if searchResult:
1315                    searchResults[iType][instrument["ticker"]] = instrument
1316
1317        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1318        info = [
1319            "# Search results\n\n",
1320            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1321            "* **Search pattern:** [{}]\n".format(pattern),
1322            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1323            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1324        ]
1325        infoShort = info[:]
1326
1327        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1328        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1329        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1330
1331        if resultsLen == 0:
1332            info.append("\nNo results\n")
1333            infoShort.append("\nNo results\n")
1334            uLogger.warning("No results. Try changing your search pattern.")
1335
1336        else:
1337            for iType in searchResults:
1338                iTypeValuesCount = len(searchResults[iType].values())
1339                if iTypeValuesCount > 0:
1340                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1341                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1342
1343                    for instrument in searchResults[iType].values():
1344                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1345                            instrument["type"],
1346                            instrument["ticker"],
1347                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1348                            instrument["figi"],
1349                        ))
1350
1351                    if iTypeValuesCount <= 5:
1352                        infoShort.extend(info[-iTypeValuesCount:])
1353
1354                    else:
1355                        infoShort.extend(info[-5:])
1356                        infoShort.append(skippedLine)
1357
1358        infoText = "".join(info)
1359        infoTextShort = "".join(infoShort)
1360
1361        if show and not onlyFiles:
1362            uLogger.info(infoTextShort)
1363            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1364
1365        if self.searchResultsFile and (show or onlyFiles):
1366            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1367                fH.write(infoText)
1368
1369            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1370
1371            if self.useHTMLReports:
1372                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1373                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1374                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1375
1376                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1377
1378        return searchResults
1379
1380    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1381        """
1382        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1383
1384        :param instruments: list of strings with tickers or FIGIs.
1385        :return: list with unique instrument FIGIs only.
1386        """
1387        requestedInstruments = []
1388        for iName in instruments:
1389            if iName not in self.aliases.keys():
1390                if iName not in requestedInstruments:
1391                    requestedInstruments.append(iName)
1392
1393            else:
1394                if iName not in requestedInstruments:
1395                    if self.aliases[iName] not in requestedInstruments:
1396                        requestedInstruments.append(self.aliases[iName])
1397
1398        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1399
1400        onlyUniqueFIGIs = []
1401        for iName in requestedInstruments:
1402            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1403                continue
1404
1405            self._ticker = iName
1406            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1407
1408            if not iData:
1409                self._ticker = ""
1410                self._figi = iName
1411
1412                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1413
1414                if not iData:
1415                    self._figi = ""
1416                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1417
1418            if iData and iData["figi"] not in onlyUniqueFIGIs:
1419                onlyUniqueFIGIs.append(iData["figi"])
1420
1421        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1422
1423        return onlyUniqueFIGIs
1424
1425    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1426        """
1427        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1428
1429        See limits: https://tinkoff.github.io/investAPI/limits/
1430
1431        If `pricesFile` string is not empty then also save information to this file.
1432
1433        :param instruments: list of strings with tickers or FIGIs.
1434        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1435        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1436        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1437                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1438        """
1439        if instruments is None or not instruments:
1440            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1441            raise Exception("Ticker or FIGI required")
1442
1443        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1444
1445        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1446
1447        iList = []  # trying to get info and current prices about all unique instruments:
1448        for self._figi in onlyUniqueFIGIs:
1449            iData = self.SearchByFIGI(requestPrice=True, show=False)
1450            iList.append(iData)
1451
1452        self.ShowListOfPrices(iList, show, onlyFiles)
1453
1454        return iList
1455
1456    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1457        """
1458        Show table contains current prices of given instruments.
1459
1460        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1461                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1462        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1463        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1464        :return: multilines text in Markdown format as a table contains current prices.
1465        """
1466        infoText = ""
1467
1468        if show or self.pricesFile or onlyFiles:
1469            info = [
1470                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1471                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1472                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1473            ]
1474
1475            for item in iList:
1476                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1477                    item["ticker"],
1478                    item["figi"],
1479                    item["type"],
1480                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1481                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1482                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1483                    "{} / {}".format(
1484                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1485                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1486                    ),
1487                    "{} / {}".format(
1488                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1489                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1490                    ),
1491                    item["currency"],
1492                ))
1493
1494            infoText = "".join(info)
1495
1496            if show and not onlyFiles:
1497                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1498
1499            if self.pricesFile and (show or onlyFiles):
1500                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1501                    fH.write(infoText)
1502
1503                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1504
1505                if self.useHTMLReports:
1506                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1507                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1508                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1509
1510                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1511
1512        return infoText
1513
1514    def RequestTradingStatus(self) -> dict:
1515        """
1516        Requesting trading status for the instrument defined by `figi` variable.
1517
1518        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1519
1520        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1521
1522        :return: dictionary with trading status attributes. Response example:
1523                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1524                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1525        """
1526        if self._figi is None or not self._figi:
1527            uLogger.error("Variable `figi` must be defined for using this method!")
1528            raise Exception("FIGI required")
1529
1530        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1531
1532        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1533        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1534        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1535
1536        if self.moreDebug:
1537            uLogger.debug("Records about current trading status successfully received")
1538
1539        return tradingStatus
1540
1541    def RequestPortfolio(self) -> dict:
1542        """
1543        Requesting actual user's portfolio for current `accountId`.
1544
1545        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1546
1547        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1548
1549        :return: dictionary with user's portfolio.
1550        """
1551        if self.accountId is None or not self.accountId:
1552            uLogger.error("Variable `accountId` must be defined for using this method!")
1553            raise Exception("Account ID required")
1554
1555        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1556
1557        self.body = str({"accountId": self.accountId})
1558        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1559        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1560
1561        if self.moreDebug:
1562            uLogger.debug("Records about user's portfolio successfully received")
1563
1564        return rawPortfolio
1565
1566    def RequestPositions(self) -> dict:
1567        """
1568        Requesting open positions by currencies and instruments for current `accountId`.
1569
1570        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1571
1572        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1573
1574        :return: dictionary with open positions by instruments.
1575        """
1576        if self.accountId is None or not self.accountId:
1577            uLogger.error("Variable `accountId` must be defined for using this method!")
1578            raise Exception("Account ID required")
1579
1580        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1581
1582        self.body = str({"accountId": self.accountId})
1583        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1584        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1585
1586        if self.moreDebug:
1587            uLogger.debug("Records about current open positions successfully received")
1588
1589        return rawPositions
1590
1591    def RequestPendingOrders(self) -> list:
1592        """
1593        Requesting current actual pending limit orders for current `accountId`.
1594
1595        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1596
1597        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1598
1599        :return: list of dictionaries with pending limit orders.
1600        """
1601        if self.accountId is None or not self.accountId:
1602            uLogger.error("Variable `accountId` must be defined for using this method!")
1603            raise Exception("Account ID required")
1604
1605        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1606
1607        self.body = str({"accountId": self.accountId})
1608        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1609        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1610
1611        if "orders" in rawResponse.keys():
1612            rawOrders = rawResponse["orders"]
1613            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1614
1615        else:
1616            rawOrders = []
1617            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1618
1619        return rawOrders
1620
1621    def RequestStopOrders(self) -> list:
1622        """
1623        Requesting current actual stop orders for current `accountId`.
1624
1625        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1626
1627        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1628
1629        :return: list of dictionaries with stop orders.
1630        """
1631        if self.accountId is None or not self.accountId:
1632            uLogger.error("Variable `accountId` must be defined for using this method!")
1633            raise Exception("Account ID required")
1634
1635        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1636
1637        self.body = str({"accountId": self.accountId})
1638        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1639        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1640
1641        if "stopOrders" in rawResponse.keys():
1642            rawStopOrders = rawResponse["stopOrders"]
1643            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1644
1645        else:
1646            rawStopOrders = []
1647            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1648
1649        return rawStopOrders
1650
1651    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1652        """
1653        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1654        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1655        and `overviewBondsCalendarFile` are defined then also save information to file.
1656
1657        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1658        many requests about the state of the portfolio, and then, based on the received data, a large number
1659        of calculation and statistics are collected.
1660
1661        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1662        :param details: how detailed should the information be?
1663        - `full` — shows full available information about portfolio status (by default),
1664        - `positions` — shows only open positions,
1665        - `orders` — shows only sections of open limits and stop orders.
1666        - `digest` — show a short digest of the portfolio status,
1667        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1668        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1669        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1670        :return: dictionary with client's raw portfolio and some statistics.
1671        """
1672        if self.accountId is None or not self.accountId:
1673            uLogger.error("Variable `accountId` must be defined for using this method!")
1674            raise Exception("Account ID required")
1675
1676        view = {
1677            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1678                "headers": {},  # list of dictionaries, response headers without "positions" section
1679                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1680                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1681                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1682                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1683                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1684                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1685                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1686                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1687                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1688            },
1689            "stat": {  # --- some statistics calculated using "raw" sections:
1690                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1691                "availableRUB": 0.,  # available rubles (without other currencies)
1692                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1693                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1694                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1695                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1696                "sharesCostRUB": 0.,  # costs of all shares in RUB
1697                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1698                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1699                "futuresCostRUB": 0.,  # costs of all futures in RUB
1700                "Currencies": [],  # list of dictionaries of all currencies statistics
1701                "Shares": [],  # list of dictionaries of all shares statistics
1702                "Bonds": [],  # list of dictionaries of all bonds statistics
1703                "Etfs": [],  # list of dictionaries of all etfs statistics
1704                "Futures": [],  # list of dictionaries of all futures statistics
1705                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1706                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1707                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1708                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1709                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1710            },
1711            "analytics": {  # --- some analytics of portfolio:
1712                "distrByAssets": {},  # portfolio distribution by assets
1713                "distrByCompanies": {},  # portfolio distribution by companies
1714                "distrBySectors": {},  # portfolio distribution by sectors
1715                "distrByCurrencies": {},  # portfolio distribution by currencies
1716                "distrByCountries": {},  # portfolio distribution by countries
1717                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1718            }
1719        }
1720
1721        details = details.lower()
1722        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1723        if details not in availableDetails:
1724            details = "full"
1725            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1726
1727        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1728
1729        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1730        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1731        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1732        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1733
1734        # save response headers without "positions" section:
1735        for key in portfolioResponse.keys():
1736            if key != "positions":
1737                view["raw"]["headers"][key] = portfolioResponse[key]
1738
1739            else:
1740                continue
1741
1742        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1743        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1744        for item in portfolioResponse["positions"]:
1745            if item["instrumentType"] == "currency":
1746                self._figi = item["figi"]
1747                if not self._figi and item["ticker"]:
1748                    self._ticker = item["ticker"]
1749                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1750
1751                curr = self.SearchByFIGI(requestPrice=False)
1752
1753                # current price of currency in RUB:
1754                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1755                    "name": curr["name"],
1756                    "currentPrice": NanoToFloat(
1757                        item["currentPrice"]["units"],
1758                        item["currentPrice"]["nano"]
1759                    ),
1760                }
1761
1762                view["raw"]["Currencies"].append(item)
1763
1764            elif item["instrumentType"] == "share":
1765                view["raw"]["Shares"].append(item)
1766
1767            elif item["instrumentType"] == "bond":
1768                view["raw"]["Bonds"].append(item)
1769
1770            elif item["instrumentType"] == "etf":
1771                view["raw"]["Etfs"].append(item)
1772
1773            elif item["instrumentType"] == "futures":
1774                view["raw"]["Futures"].append(item)
1775
1776            else:
1777                continue
1778
1779        # how many volume of currencies (by ISO currency name) are blocked:
1780        for item in view["raw"]["positions"]["blocked"]:
1781            blocked = NanoToFloat(item["units"], item["nano"])
1782            if blocked > 0:
1783                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1784
1785        # how many volume of instruments (by FIGI) are blocked:
1786        for item in view["raw"]["positions"]["securities"]:
1787            blocked = int(item["blocked"])
1788            if blocked > 0:
1789                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1790
1791        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1792
1793        if "rub" in allBlocked.keys():
1794            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1795
1796        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1797        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1798        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1799        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1800        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1801        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1802        view["stat"]["portfolioCostRUB"] = sum([
1803            view["stat"]["allCurrenciesCostRUB"],
1804            view["stat"]["sharesCostRUB"],
1805            view["stat"]["bondsCostRUB"],
1806            view["stat"]["etfsCostRUB"],
1807            view["stat"]["futuresCostRUB"],
1808        ])
1809
1810        # --- calculating some portfolio statistics:
1811        byComp = {}  # distribution by companies
1812        bySect = {}  # distribution by sectors
1813        byCurr = {}  # distribution by currencies (include RUB)
1814        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1815        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1816
1817        for item in portfolioResponse["positions"]:
1818            self._figi = item["figi"]
1819            if not self._figi and item["ticker"]:
1820                self._ticker = item["ticker"]
1821                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1822
1823            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1824
1825            if instrument:
1826                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1827                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1828
1829                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1830                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1831
1832                else:
1833                    blocked = 0
1834
1835                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1836                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1837                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1838                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1839                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1840                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1841                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1842                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1843                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1844                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1845                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1846                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1847
1848                statData = {
1849                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1850                    "ticker": instrument["ticker"],  # ticker by FIGI
1851                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1852                    "volume": volume,  # available volume of instrument
1853                    "lots": lots,  # volume in lots of instrument
1854                    "direction": direction,  # direction of an instrument's position: short or long
1855                    "blocked": blocked,  # blocked volume of currency or instrument
1856                    "currentPrice": curPrice,  # current instrument's price in basic asset
1857                    "average": average,  # current average position price
1858                    "cost": cost,  # current cost of all volume of instrument in basic asset
1859                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1860                    "costRUB": costRUB,  # cost of instrument in ruble
1861                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1862                    "profit": profit,  # expected profit at current moment
1863                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1864                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1865                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1866                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1867                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1868                    "step": instrument["step"],  # minimum price increment
1869                }
1870
1871                # adding distribution by unique countries:
1872                if statData["country"] not in byCountry.keys():
1873                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1874
1875                else:
1876                    byCountry[statData["country"]]["cost"] += costRUB
1877                    byCountry[statData["country"]]["percent"] += percentCostRUB
1878
1879                if item["instrumentType"] != "currency":
1880                    # adding distribution by unique companies:
1881                    if statData["name"]:
1882                        if statData["name"] not in byComp.keys():
1883                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1884
1885                        else:
1886                            byComp[statData["name"]]["cost"] += costRUB
1887                            byComp[statData["name"]]["percent"] += percentCostRUB
1888
1889                    # adding distribution by unique sectors:
1890                    if statData["sector"] not in bySect.keys():
1891                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1892
1893                    else:
1894                        bySect[statData["sector"]]["cost"] += costRUB
1895                        bySect[statData["sector"]]["percent"] += percentCostRUB
1896
1897                # adding distribution by unique currencies:
1898                if currency not in byCurr.keys():
1899                    byCurr[currency] = {
1900                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1901                        "cost": costRUB,
1902                        "percent": percentCostRUB
1903                    }
1904
1905                else:
1906                    byCurr[currency]["cost"] += costRUB
1907                    byCurr[currency]["percent"] += percentCostRUB
1908
1909                # saving statistics for every instrument:
1910                if item["instrumentType"] == "currency":
1911                    view["stat"]["Currencies"].append(statData)
1912
1913                    # update dict with free funds for trading (total - blocked) by currencies
1914                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1915                    view["stat"]["funds"][currency] = {
1916                        "total": volume,
1917                        "totalCostRUB": costRUB,  # total volume cost in rubles
1918                        "free": volume - blocked,
1919                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1920                    }
1921
1922                elif item["instrumentType"] == "share":
1923                    view["stat"]["Shares"].append(statData)
1924
1925                elif item["instrumentType"] == "bond":
1926                    view["stat"]["Bonds"].append(statData)
1927
1928                elif item["instrumentType"] == "etf":
1929                    view["stat"]["Etfs"].append(statData)
1930
1931                elif item["instrumentType"] == "Futures":
1932                    view["stat"]["Futures"].append(statData)
1933
1934                else:
1935                    continue
1936
1937        # total changes in Russian Ruble:
1938        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1939        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1940        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1941        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1942        view["stat"]["funds"]["rub"] = {
1943            "total": view["stat"]["availableRUB"],
1944            "totalCostRUB": view["stat"]["availableRUB"],
1945            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1946            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1947        }
1948
1949        # --- pending limit orders sector data:
1950        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1951        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1952
1953        for item in view["raw"]["orders"]:
1954            self._figi = item["figi"]
1955
1956            if item["figi"] not in uniquePendingOrdersFIGIs:
1957                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1958
1959                uniquePendingOrdersFIGIs.append(item["figi"])
1960                uniquePendingOrders[item["figi"]] = instrument
1961
1962            else:
1963                instrument = uniquePendingOrders[item["figi"]]
1964
1965            if instrument:
1966                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1967                orderType = TKS_ORDER_TYPES[item["orderType"]]
1968                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1969                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1970
1971                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1972                if item["direction"] == "ORDER_DIRECTION_BUY":
1973                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1974
1975                else:
1976                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1977
1978                # requested price for order execution:
1979                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1980
1981                # necessary changes in percent to reach target from current price:
1982                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1983
1984                view["stat"]["orders"].append({
1985                    "orderID": item["orderId"],  # orderId number parameter of current order
1986                    "figi": item["figi"],  # FIGI identification
1987                    "ticker": instrument["ticker"],  # ticker name by FIGI
1988                    "lotsRequested": item["lotsRequested"],  # requested lots value
1989                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1990                    "currentPrice": lastPrice,  # current instrument's price for defined action
1991                    "targetPrice": target,  # requested price for order execution in base currency
1992                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1993                    "percentChanges": changes,  # changes in percent to target from current price
1994                    "currency": item["currency"],  # instrument's currency name
1995                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1996                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1997                    "status": orderState,  # order status from TKS_ORDER_STATES
1998                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1999                })
2000
2001        # --- stop orders sector data:
2002        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
2003        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
2004
2005        for item in view["raw"]["stopOrders"]:
2006            self._figi = item["figi"]
2007
2008            if item["figi"] not in uniqueStopOrdersFIGIs:
2009                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
2010
2011                uniqueStopOrdersFIGIs.append(item["figi"])
2012                uniqueStopOrders[item["figi"]] = instrument
2013
2014            else:
2015                instrument = uniqueStopOrders[item["figi"]]
2016
2017            if instrument:
2018                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
2019                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
2020                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
2021
2022                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
2023                if "expirationTime" in item.keys():
2024                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
2025                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
2026
2027                else:
2028                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
2029                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2030
2031                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2032                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2033                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2034
2035                else:
2036                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2037
2038                # requested price when stop-order executed:
2039                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2040
2041                # price for limit-order, set up when stop-order executed:
2042                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2043
2044                # necessary changes in percent to reach target from current price:
2045                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2046
2047                view["stat"]["stopOrders"].append({
2048                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2049                    "figi": item["figi"],  # FIGI identification
2050                    "ticker": instrument["ticker"],  # ticker name by FIGI
2051                    "lotsRequested": item["lotsRequested"],  # requested lots value
2052                    "currentPrice": lastPrice,  # current instrument's price for defined action
2053                    "targetPrice": target,  # requested price for stop-order execution in base currency
2054                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2055                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2056                    "percentChanges": changes,  # changes in percent to target from current price
2057                    "currency": item["currency"],  # instrument's currency name
2058                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2059                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2060                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2061                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2062                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2063                })
2064
2065        # --- calculating data for analytics section:
2066        # portfolio distribution by assets:
2067        view["analytics"]["distrByAssets"] = {
2068            "Ruble": {
2069                "uniques": 1,
2070                "cost": view["stat"]["availableRUB"],
2071                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2072            },
2073            "Currencies": {
2074                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2075                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2076                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2077            },
2078            "Shares": {
2079                "uniques": len(view["stat"]["Shares"]),
2080                "cost": view["stat"]["sharesCostRUB"],
2081                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2082            },
2083            "Bonds": {
2084                "uniques": len(view["stat"]["Bonds"]),
2085                "cost": view["stat"]["bondsCostRUB"],
2086                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2087            },
2088            "Etfs": {
2089                "uniques": len(view["stat"]["Etfs"]),
2090                "cost": view["stat"]["etfsCostRUB"],
2091                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2092            },
2093            "Futures": {
2094                "uniques": len(view["stat"]["Futures"]),
2095                "cost": view["stat"]["futuresCostRUB"],
2096                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2097            },
2098        }
2099
2100        # portfolio distribution by companies:
2101        view["analytics"]["distrByCompanies"]["All money cash"] = {
2102            "ticker": "",
2103            "cost": view["stat"]["allCurrenciesCostRUB"],
2104            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2105        }
2106        view["analytics"]["distrByCompanies"].update(byComp)
2107
2108        # portfolio distribution by sectors:
2109        view["analytics"]["distrBySectors"]["All money cash"] = {
2110            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2111            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2112        }
2113        view["analytics"]["distrBySectors"].update(bySect)
2114
2115        # portfolio distribution by currencies:
2116        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2117            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2118
2119            if self.moreDebug:
2120                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2121
2122        view["analytics"]["distrByCurrencies"].update(byCurr)
2123        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2124        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2125
2126        # portfolio distribution by countries:
2127        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2128            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2129
2130            if self.moreDebug:
2131                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2132
2133        view["analytics"]["distrByCountries"].update(byCountry)
2134        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2135        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2136
2137        # --- Prepare text statistics overview in human-readable:
2138        if show or onlyFiles:
2139            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2140
2141            # Whatever the value `details`, header not changes:
2142            info = [
2143                "# Client's portfolio\n\n",
2144                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2145                "* **Account ID:** [{}]\n".format(self.accountId),
2146            ]
2147
2148            if details in ["full", "positions", "digest"]:
2149                info.extend([
2150                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2151                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2152                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2153                        view["stat"]["totalChangesRUB"],
2154                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2155                        view["stat"]["totalChangesPercentRUB"],
2156                    ),
2157                ])
2158
2159            if details in ["full", "positions"]:
2160                info.extend([
2161                    "## Open positions\n\n",
2162                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2163                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2164                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2165                        "{:.2f} ({:.2f}) rub".format(
2166                            view["stat"]["availableRUB"],
2167                            view["stat"]["blockedRUB"],
2168                        )
2169                    )
2170                ])
2171
2172                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2173                    return [
2174                        "|                             |                                 |          |              |              |                     |                              |\n",
2175                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2176                            noTradeStr if noTradeStr else typeStr,
2177                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2178                        ),
2179                    ]
2180
2181                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2182                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2183                        "{} [{}]".format(data["ticker"], data["figi"]),
2184                        "{:.2f} ({:.2f}) {}".format(
2185                            data["volume"],
2186                            data["blocked"],
2187                            data["currency"],
2188                        ) if isCurr else "{:.0f} ({:.0f})".format(
2189                            data["volume"],
2190                            data["blocked"],
2191                        ),
2192                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2193                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2194                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2195                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2196                        "{}{:.2f} {} ({}{:.2f}%)".format(
2197                            "+" if data["profit"] > 0 else "",
2198                            data["profit"], data["baseCurrencyName"],
2199                            "+" if data["percentProfit"] > 0 else "",
2200                            data["percentProfit"],
2201                        ),
2202                    )
2203
2204                # --- Show currencies section:
2205                if view["stat"]["Currencies"]:
2206                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2207                    for item in view["stat"]["Currencies"]:
2208                        info.append(_InfoStr(item, isCurr=True))
2209
2210                else:
2211                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2212
2213                # --- Show shares section:
2214                if view["stat"]["Shares"]:
2215                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2216
2217                    for item in view["stat"]["Shares"]:
2218                        info.append(_InfoStr(item))
2219
2220                else:
2221                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2222
2223                # --- Show bonds section:
2224                if view["stat"]["Bonds"]:
2225                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2226
2227                    for item in view["stat"]["Bonds"]:
2228                        info.append(_InfoStr(item))
2229
2230                else:
2231                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2232
2233                # --- Show etfs section:
2234                if view["stat"]["Etfs"]:
2235                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2236
2237                    for item in view["stat"]["Etfs"]:
2238                        info.append(_InfoStr(item))
2239
2240                else:
2241                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2242
2243                # --- Show futures section:
2244                if view["stat"]["Futures"]:
2245                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2246
2247                    for item in view["stat"]["Futures"]:
2248                        info.append(_InfoStr(item))
2249
2250                else:
2251                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2252
2253            if details in ["full", "orders"]:
2254                # --- Show pending limit orders section:
2255                if view["stat"]["orders"]:
2256                    info.extend([
2257                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2258                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2259                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2260                    ])
2261
2262                    for item in view["stat"]["orders"]:
2263                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2264                            "{} [{}]".format(item["ticker"], item["figi"]),
2265                            item["orderID"],
2266                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2267                            "{} {} ({}{:.2f}%)".format(
2268                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2269                                item["baseCurrencyName"],
2270                                "+" if item["percentChanges"] > 0 else "",
2271                                float(item["percentChanges"]),
2272                            ),
2273                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2274                            item["action"],
2275                            item["type"],
2276                            item["date"],
2277                        ))
2278
2279                else:
2280                    info.append("\n## Total pending limit-orders: [0]\n")
2281
2282                # --- Show stop orders section:
2283                if view["stat"]["stopOrders"]:
2284                    info.extend([
2285                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2286                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2287                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2288                    ])
2289
2290                    for item in view["stat"]["stopOrders"]:
2291                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2292                            "{} [{}]".format(item["ticker"], item["figi"]),
2293                            item["orderID"],
2294                            item["lotsRequested"],
2295                            "{} {} ({}{:.2f}%)".format(
2296                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2297                                item["baseCurrencyName"],
2298                                "+" if item["percentChanges"] > 0 else "",
2299                                float(item["percentChanges"]),
2300                            ),
2301                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2302                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2303                            item["action"],
2304                            item["type"],
2305                            item["expType"],
2306                            item["createDate"],
2307                            item["expDate"],
2308                        ))
2309
2310                else:
2311                    info.append("\n## Total stop-orders: [0]\n")
2312
2313            if details in ["full", "analytics"]:
2314                # -- Show analytics section:
2315                if view["stat"]["portfolioCostRUB"] > 0:
2316                    info.extend([
2317                        "\n# Analytics\n\n"
2318                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2319                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2320                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2321                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2322                            view["stat"]["totalChangesRUB"],
2323                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2324                            view["stat"]["totalChangesPercentRUB"],
2325                        ),
2326                        "\n## Portfolio distribution by assets\n"
2327                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2328                        "|------------------------------------|---------|---------|--------------------|\n",
2329                    ])
2330
2331                    for key in view["analytics"]["distrByAssets"].keys():
2332                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2333                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2334                                key,
2335                                view["analytics"]["distrByAssets"][key]["uniques"],
2336                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2337                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2338                            ))
2339
2340                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2341
2342                    info.extend([
2343                        "\n## Portfolio distribution by companies\n"
2344                        "\n| Company                                      | Percent | Current cost       |\n",
2345                        aSepLine,
2346                    ])
2347
2348                    for company in view["analytics"]["distrByCompanies"].keys():
2349                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2350                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2351                                "{}{}".format(
2352                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2353                                    company,
2354                                ),
2355                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2356                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2357                            ))
2358
2359                    info.extend([
2360                        "\n## Portfolio distribution by sectors\n"
2361                        "\n| Sector                                       | Percent | Current cost       |\n",
2362                        aSepLine,
2363                    ])
2364
2365                    for sector in view["analytics"]["distrBySectors"].keys():
2366                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2367                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2368                                sector,
2369                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2370                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2371                            ))
2372
2373                    info.extend([
2374                        "\n## Portfolio distribution by currencies\n"
2375                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2376                        aSepLine,
2377                    ])
2378
2379                    for curr in view["analytics"]["distrByCurrencies"].keys():
2380                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2381                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2382                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2383                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2384                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2385                            ))
2386
2387                    info.extend([
2388                        "\n## Portfolio distribution by countries\n"
2389                        "\n| Assets by country                            | Percent | Current cost       |\n",
2390                        aSepLine,
2391                    ])
2392
2393                    for country in view["analytics"]["distrByCountries"].keys():
2394                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2395                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2396                                country,
2397                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2398                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2399                            ))
2400
2401            if details in ["full", "calendar"]:
2402                # -- Show bonds payment calendar section:
2403                if view["stat"]["Bonds"]:
2404                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2405                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2406                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2407
2408                else:
2409                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2410
2411            infoText = "".join(info)
2412
2413            if show and not onlyFiles:
2414                uLogger.info(infoText)
2415
2416            if details == "full" and self.overviewFile:
2417                filename = self.overviewFile
2418
2419            elif details == "digest" and self.overviewDigestFile:
2420                filename = self.overviewDigestFile
2421
2422            elif details == "positions" and self.overviewPositionsFile:
2423                filename = self.overviewPositionsFile
2424
2425            elif details == "orders" and self.overviewOrdersFile:
2426                filename = self.overviewOrdersFile
2427
2428            elif details == "analytics" and self.overviewAnalyticsFile:
2429                filename = self.overviewAnalyticsFile
2430
2431            elif details == "calendar" and self.overviewBondsCalendarFile:
2432                filename = self.overviewBondsCalendarFile
2433
2434            else:
2435                filename = ""
2436
2437            if filename and (show or onlyFiles):
2438                with open(filename, "w", encoding="UTF-8") as fH:
2439                    fH.write(infoText)
2440
2441                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2442
2443                if self.useHTMLReports:
2444                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2445                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2446                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2447
2448                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2449
2450        return view
2451
2452    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2453        """
2454        Returns history operations between two given dates for current `accountId`.
2455        If `reportFile` string is not empty then also save human-readable report.
2456        Shows some statistical data of closed positions.
2457
2458        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2459        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2460        :param show: if `True` then also prints all records to the console.
2461        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2462        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2463        :return: original list of dictionaries with history of deals records from API ("operations" key):
2464                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2465                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2466        """
2467        if self.accountId is None or not self.accountId:
2468            uLogger.error("Variable `accountId` must be defined for using this method!")
2469            raise Exception("Account ID required")
2470
2471        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2472
2473        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2474
2475        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2476        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2477        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2478        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2479        customStat = {}  # custom statistics in additional to responseJSON
2480
2481        # --- output report in human-readable format:
2482        if self.reportFile and (show or onlyFiles):
2483            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2484            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2485            nextDay = ""
2486
2487            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2488
2489            if len(ops) > 0:
2490                customStat = {
2491                    "opsCount": 0,  # total operations count
2492                    "buyCount": 0,  # buy operations
2493                    "sellCount": 0,  # sell operations
2494                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2495                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2496                    "payIn": {"rub": 0.},  # Deposit brokerage account
2497                    "payOut": {"rub": 0.},  # Withdrawals
2498                    "divs": {"rub": 0.},  # Dividends income
2499                    "coupons": {"rub": 0.},  # Coupon's income
2500                    "brokerCom": {"rub": 0.},  # Service commissions
2501                    "serviceCom": {"rub": 0.},  # Service commissions
2502                    "marginCom": {"rub": 0.},  # Margin commissions
2503                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2504                }
2505
2506                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2507                for item in ops:
2508                    if item["state"] == "OPERATION_STATE_EXECUTED":
2509                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2510
2511                        # count buy operations:
2512                        if "_BUY" in item["operationType"]:
2513                            customStat["buyCount"] += 1
2514
2515                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2516                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2517
2518                            else:
2519                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2520
2521                        # count sell operations:
2522                        elif "_SELL" in item["operationType"]:
2523                            customStat["sellCount"] += 1
2524
2525                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2526                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2527
2528                            else:
2529                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2530
2531                        # count incoming operations:
2532                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2533                            if item["payment"]["currency"] in customStat["payIn"].keys():
2534                                customStat["payIn"][item["payment"]["currency"]] += payment
2535
2536                            else:
2537                                customStat["payIn"][item["payment"]["currency"]] = payment
2538
2539                        # count withdrawals operations:
2540                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2541                            if item["payment"]["currency"] in customStat["payOut"].keys():
2542                                customStat["payOut"][item["payment"]["currency"]] += payment
2543
2544                            else:
2545                                customStat["payOut"][item["payment"]["currency"]] = payment
2546
2547                        # count dividends income:
2548                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2549                            if item["payment"]["currency"] in customStat["divs"].keys():
2550                                customStat["divs"][item["payment"]["currency"]] += payment
2551
2552                            else:
2553                                customStat["divs"][item["payment"]["currency"]] = payment
2554
2555                        # count coupon's income:
2556                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2557                            if item["payment"]["currency"] in customStat["coupons"].keys():
2558                                customStat["coupons"][item["payment"]["currency"]] += payment
2559
2560                            else:
2561                                customStat["coupons"][item["payment"]["currency"]] = payment
2562
2563                        # count broker commissions:
2564                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2565                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2566                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2567
2568                            else:
2569                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2570
2571                        # count service commissions:
2572                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2573                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2574                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2575
2576                            else:
2577                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2578
2579                        # count margin commissions:
2580                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2581                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2582                                customStat["marginCom"][item["payment"]["currency"]] += payment
2583
2584                            else:
2585                                customStat["marginCom"][item["payment"]["currency"]] = payment
2586
2587                        # count withholding taxes:
2588                        elif "_TAX" in item["operationType"]:
2589                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2590                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2591
2592                            else:
2593                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2594
2595                        else:
2596                            continue
2597
2598                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2599
2600                # --- view "Actions" lines:
2601                info.extend([
2602                    "| Report sections            |                               |                              |                      |                        |\n",
2603                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2604                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2605                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2606                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2607                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2608                    ),
2609                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2610                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2611                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2612                    ),
2613                ])
2614
2615                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2616                for key in opsKeys:
2617                    if key == "rub":
2618                        continue
2619
2620                    info.extend([
2621                        "|                            |                               | {:<28} |                      |                        |\n".format(
2622                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2623                        ),
2624                        "|                            |                               | {:<28} |                      |                        |\n".format(
2625                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2626                        ),
2627                    ])
2628
2629                info.append(splitLine1)
2630
2631                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2632                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2633                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2634                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2635                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2636                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2637                    )
2638
2639                # --- view "Payments" lines:
2640                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2641                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2642
2643                for key in paymentsKeys:
2644                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2645
2646                info.append(splitLine1)
2647
2648                # --- view "Commissions and taxes" lines:
2649                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2650                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2651
2652                for key in comKeys:
2653                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2654
2655                info.extend([
2656                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2657                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2658                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2659                ])
2660
2661            else:
2662                info.append("Broker returned no operations during this period\n")
2663
2664            # --- view "Operations" section:
2665            for item in ops:
2666                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2667                    continue
2668
2669                else:
2670                    self._figi = item["figi"]
2671                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2672                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2673
2674                    # group of deals during one day:
2675                    if nextDay and item["date"].split("T")[0] != nextDay:
2676                        info.append(splitLine2)
2677                        nextDay = ""
2678
2679                    else:
2680                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2681
2682                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2683                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2684                        self._figi if self._figi else "—",
2685                        instrument["ticker"] if instrument else "—",
2686                        instrument["type"] if instrument else "—",
2687                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2688                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2689                        TKS_OPERATION_STATES[item["state"]],
2690                        TKS_OPERATION_TYPES[item["operationType"]],
2691                    ))
2692
2693            infoText = "".join(info)
2694
2695            if show and not onlyFiles:
2696                if self.moreDebug:
2697                    uLogger.debug("Records about history of a client's operations successfully received")
2698
2699                uLogger.info(infoText)
2700
2701            if self.reportFile and (show or onlyFiles):
2702                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2703                    fH.write(infoText)
2704
2705                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2706
2707                if self.useHTMLReports:
2708                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2709                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2710                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2711
2712                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2713
2714        return ops, customStat
2715
2716    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2717        """
2718        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2719
2720        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2721        Warning! Broker server used ISO UTC time by default.
2722
2723        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2724        Also, `historyFile` used to update history with `onlyMissing` parameter.
2725
2726        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2727
2728        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2729        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2730        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2731                         `"hour"`, `"day"`. Default: `"hour"`.
2732        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2733                            False by default. Warning! History appends only from last candle to current time
2734                            with always update last candle!
2735        :param csvSep: separator if csv-file is used, `,` by default.
2736        :param show: if `True` then also prints Pandas DataFrame to the console.
2737        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2738        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2739                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2740        """
2741        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2742        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2743        history = None  # empty pandas object for history
2744
2745        if interval not in TKS_CANDLE_INTERVALS.keys():
2746            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2747            raise Exception("Incorrect value")
2748
2749        if not (self._ticker or self._figi):
2750            uLogger.error("Ticker or FIGI must be defined!")
2751            raise Exception("Ticker or FIGI required")
2752
2753        if self._ticker and not self._figi:
2754            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2755            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2756
2757        if self._figi and not self._ticker:
2758            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2759            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2760
2761        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2762        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2763        if interval.lower() != "day":
2764            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2765
2766        delta = dtEnd - dtStart  # current UTC time minus last time in file
2767        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2768
2769        # calculate history length in candles:
2770        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2771        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2772            length += 1  # to avoid fraction time
2773
2774        # calculate data blocks count:
2775        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2776
2777        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2778        if self.moreDebug:
2779            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2780            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2781            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2782            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2783
2784        tempOld = None  # pandas object for old history, if --only-missing key present
2785        lastTime = None  # datetime object of last old candle in file
2786
2787        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2788            if self.moreDebug:
2789                uLogger.debug("--only-missing key present, add only last missing candles...")
2790                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2791
2792            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2793
2794            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2795            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2796            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2797            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2798
2799            # get last datetime object from last string in file or minus 1 delta if file is empty:
2800            if len(tempOld) > 0:
2801                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2802
2803            else:
2804                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2805
2806            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2807
2808        responseJSONs = []  # raw history blocks of data
2809
2810        blockEnd = dtEnd
2811        for item in range(blocks):
2812            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2813            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2814
2815            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2816                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2817            ))
2818
2819            if blockStart == blockEnd:
2820                uLogger.debug("Skipped this zero-length block...")
2821
2822            else:
2823                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2824                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2825                self.body = str({
2826                    "figi": self._figi,
2827                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2828                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2829                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2830                })
2831                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2832
2833                if "code" in responseJSON.keys():
2834                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2835
2836                else:
2837                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2838                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2839
2840                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2841
2842            blockEnd = blockStart
2843
2844        printCount = len(responseJSONs)  # candles to show in console
2845        if responseJSONs:
2846            tempHistory = pd.DataFrame(
2847                data={
2848                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2849                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2850                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2851                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2852                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2853                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2854                    "volume": [int(item["volume"]) for item in responseJSONs],
2855                },
2856                index=range(len(responseJSONs)),
2857                columns=["date", "time", "open", "high", "low", "close", "volume"],
2858            )
2859            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2860            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2861
2862            # append only newest candles to old history if --only-missing key present:
2863            if onlyMissing and tempOld is not None and lastTime is not None:
2864                index = 0  # find start index in tempHistory data:
2865
2866                for i, item in tempHistory.iterrows():
2867                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2868
2869                    if curTime == lastTime:
2870                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2871                        index = i
2872                        printCount = index + 1
2873                        break
2874
2875                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2876
2877            else:
2878                history = tempHistory  # if no `--only-missing` key then load full data from server
2879
2880            if self.moreDebug:
2881                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2882
2883        if history is not None and not history.empty:
2884            if show and not onlyFiles:
2885                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2886                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2887                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2888                ))
2889
2890        else:
2891            uLogger.warning("Received an empty candles history!")
2892
2893        if self.historyFile is not None:
2894            if history is not None and not history.empty:
2895                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2896                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2897
2898            else:
2899                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2900
2901        else:
2902            if self.moreDebug:
2903                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2904
2905        return history
2906
2907    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2908        """
2909        Load candles history from csv-file and return Pandas DataFrame object.
2910
2911        See also: `History()` and `ShowHistoryChart()` methods.
2912
2913        :param filePath: path to csv-file to open.
2914        """
2915        loadedHistory = None  # init candles data object
2916
2917        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2918
2919        if os.path.exists(filePath):
2920            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2921
2922            tfStr = self.priceModel.FormattedDelta(
2923                self.priceModel.timeframe,
2924                "{days} days {hours}h {minutes}m {seconds}s",
2925            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2926                self.priceModel.timeframe,
2927                "{hours}h {minutes}m {seconds}s",
2928            )
2929
2930            if loadedHistory is not None and not loadedHistory.empty:
2931                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2932                    len(loadedHistory),
2933                    tfStr,
2934                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2935                )
2936
2937            else:
2938                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2939
2940        else:
2941            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2942
2943        return loadedHistory
2944
2945    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2946        """
2947        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2948
2949        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2950        Default: `index.html` (both for interact and non-interact candlesticks chart).
2951
2952        See also: `History()` and `LoadHistory()` methods.
2953
2954        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2955        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2956                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2957                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2958                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2959        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2960                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2961        """
2962        if isinstance(candles, str):
2963            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2964            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2965
2966        elif isinstance(candles, pd.DataFrame):
2967            self.priceModel.prices = candles  # set candles chain from variable
2968            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2969
2970            if "datetime" not in candles.columns:
2971                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2972
2973        else:
2974            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2975            raise Exception("Incorrect value")
2976
2977        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2978
2979        if interact:
2980            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2981
2982            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2983
2984        else:
2985            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2986
2987            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2988
2989        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2990
2991    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2992        """
2993        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2994        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2995
2996        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2997
2998        :param operation: string "Buy" or "Sell".
2999        :param lots: volume, integer count of lots >= 1.
3000        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
3001        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
3002        :param expDate: string "Undefined" by default or local date in future,
3003                        it is a string with format `%Y-%m-%d %H:%M:%S`.
3004        :return: JSON with response from broker server.
3005        """
3006        if self.accountId is None or not self.accountId:
3007            uLogger.error("Variable `accountId` must be defined for using this method!")
3008            raise Exception("Account ID required")
3009
3010        if operation is None or not operation or operation not in ("Buy", "Sell"):
3011            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3012            raise Exception("Incorrect value")
3013
3014        if lots is None or lots < 1:
3015            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
3016            lots = 1
3017
3018        if tp is None or tp < 0:
3019            tp = 0
3020
3021        if sl is None or sl < 0:
3022            sl = 0
3023
3024        if expDate is None or not expDate:
3025            expDate = "Undefined"
3026
3027        if not (self._ticker or self._figi):
3028            uLogger.error("Ticker or FIGI must be defined!")
3029            raise Exception("Ticker or FIGI required")
3030
3031        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3032        self._ticker = instrument["ticker"]
3033        self._figi = instrument["figi"]
3034
3035        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3036
3037        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3038        self.body = str({
3039            "figi": self._figi,
3040            "quantity": str(lots),
3041            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3042            "accountId": str(self.accountId),
3043            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3044        })
3045        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3046
3047        if "orderId" in response.keys():
3048            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3049                operation, response["orderId"],
3050                self._ticker, self._figi, lots,
3051                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3052                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3053                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3054            ))
3055
3056            if tp > 0:
3057                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3058
3059            if sl > 0:
3060                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3061
3062        else:
3063            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3064
3065        return response
3066
3067    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3068        """
3069        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3070        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3071
3072        See also: `Order()` and `Trade()` docstrings.
3073
3074        :param lots: volume, integer count of lots >= 1.
3075        :param tp: float > 0, take profit price of stop-order.
3076        :param sl: float > 0, stop loss price of stop-order.
3077        :param expDate: it's a local date in future.
3078                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3079        :return: JSON with response from broker server.
3080        """
3081        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3082
3083    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3084        """
3085        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3086        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3087
3088        See also: `Order()` and `Trade()` docstrings.
3089
3090        :param lots: volume, integer count of lots >= 1.
3091        :param tp: float > 0, take profit price of stop-order.
3092        :param sl: float > 0, stop loss price of stop-order.
3093        :param expDate: it's a local date in the future.
3094                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3095        :return: JSON with response from broker server.
3096        """
3097        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3098
3099    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3100        """
3101        Close position of given instruments.
3102
3103        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3104        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3105                         This avoids unnecessary downloading data from the server.
3106        """
3107        if instruments is None or not instruments:
3108            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3109            raise Exception("Ticker or FIGI required")
3110
3111        if isinstance(instruments, str):
3112            instruments = [instruments]
3113
3114        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3115        if uniqueInstruments:
3116            if portfolio is None or not portfolio:
3117                portfolio = self.Overview(show=False)
3118
3119            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3120            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3121
3122            for self._figi in uniqueInstruments:
3123                if self._figi not in allOpened:
3124                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3125                    continue
3126
3127                # search open trade info about instrument by ticker:
3128                instrument = {}
3129                for iType in TKS_INSTRUMENTS:
3130                    if instrument:
3131                        break
3132
3133                    for item in portfolio["stat"][iType]:
3134                        if item["figi"] == self._figi:
3135                            instrument = item
3136                            break
3137
3138                if instrument:
3139                    self._ticker = instrument["ticker"]
3140                    self._figi = instrument["figi"]
3141
3142                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3143                        self._ticker,
3144                        self._figi,
3145                        int(instrument["volume"]),
3146                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3147                    ))
3148
3149                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3150
3151                    if tradeLots > 0:
3152                        if instrument["blocked"] > 0:
3153                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3154                                instrument["blocked"],
3155                                self._ticker,
3156                                tradeLots,
3157                            ))
3158
3159                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3160                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3161
3162                    else:
3163                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3164
3165    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3166        """
3167        Close all positions of given instruments with defined type.
3168
3169        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3170        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3171                         This avoids unnecessary downloading data from the server.
3172        """
3173        if iType not in TKS_INSTRUMENTS:
3174            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3175
3176        else:
3177            if portfolio is None or not portfolio:
3178                portfolio = self.Overview(show=False)
3179
3180            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3181            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3182
3183            if tickers and portfolio:
3184                self.CloseTrades(tickers, portfolio)
3185
3186            else:
3187                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3188
3189    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3190        """
3191        Universal method to create market or limit orders with all available parameters for current `accountId`.
3192        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3193
3194        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3195        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3196
3197        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3198        then broker immediately open market order as you can do simple --buy or --sell operations!
3199
3200        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3201        When current price will go up or down to target price value then broker opens a limit order.
3202        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3203
3204        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3205
3206        :param operation: string "Buy" or "Sell".
3207        :param orderType: string "Limit" or "Stop".
3208        :param lots: volume, integer count of lots >= 1.
3209        :param targetPrice: target price > 0. This is open trade price for limit order.
3210        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3211                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3212        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3213                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3214                         Stop loss order always executed by market price.
3215        :param expDate: string "Undefined" by default or local date in future.
3216                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3217                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3218                        A limit order has no expiration date, it lasts until the end of the trading day.
3219        :return: JSON with response from broker server.
3220        """
3221        if self.accountId is None or not self.accountId:
3222            uLogger.error("Variable `accountId` must be defined for using this method!")
3223            raise Exception("Account ID required")
3224
3225        if operation is None or not operation or operation not in ("Buy", "Sell"):
3226            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3227            raise Exception("Incorrect value")
3228
3229        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3230            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3231            raise Exception("Incorrect value")
3232
3233        if lots is None or lots < 1:
3234            uLogger.error("You must define trade volume > 0: integer count of lots!")
3235            raise Exception("Incorrect value")
3236
3237        if targetPrice is None or targetPrice <= 0:
3238            uLogger.error("Target price for limit-order must be greater than 0!")
3239            raise Exception("Incorrect value")
3240
3241        if limitPrice is None or limitPrice <= 0:
3242            limitPrice = targetPrice
3243
3244        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3245            stopType = "Limit"
3246
3247        if expDate is None or not expDate:
3248            expDate = "Undefined"
3249
3250        if not (self._ticker or self._figi):
3251            uLogger.error("Tocker or FIGI must be defined!")
3252            raise Exception("Ticker or FIGI required")
3253
3254        response = {}
3255        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3256        self._ticker = instrument["ticker"]
3257        self._figi = instrument["figi"]
3258
3259        if orderType == "Limit":
3260            uLogger.debug(
3261                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3262                    self._ticker, self._figi,
3263                    operation, lots, targetPrice, instrument["currency"],
3264                ))
3265
3266            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3267            self.body = str({
3268                "figi": self._figi,
3269                "quantity": str(lots),
3270                "price": FloatToNano(targetPrice),
3271                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3272                "accountId": str(self.accountId),
3273                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3274            })
3275            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3276
3277            if "orderId" in response.keys():
3278                uLogger.info(
3279                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3280                        response["orderId"], self._ticker, self._figi, operation, lots,
3281                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3282                    ))
3283
3284                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3285                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3286                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3287                            targetPrice, instrument["currency"],
3288                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3289                        ))
3290
3291                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3292                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3293                            targetPrice, instrument["currency"],
3294                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3295                        ))
3296
3297            else:
3298                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3299
3300        if orderType == "Stop":
3301            uLogger.debug(
3302                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3303                    self._ticker, self._figi,
3304                    operation, lots,
3305                    targetPrice, instrument["currency"],
3306                    limitPrice, instrument["currency"],
3307                    stopType, expDate,
3308                ))
3309
3310            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3311            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3312            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3313
3314            body = {
3315                "figi": self._figi,
3316                "quantity": str(lots),
3317                "price": FloatToNano(limitPrice),
3318                "stopPrice": FloatToNano(targetPrice),
3319                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3320                "accountId": str(self.accountId),
3321                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3322                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3323            }
3324
3325            if expDateUTC:
3326                body["expireDate"] = expDateUTC
3327
3328            self.body = str(body)
3329            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3330
3331            if "stopOrderId" in response.keys():
3332                uLogger.info(
3333                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3334                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3335                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3336                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3337                        TKS_STOP_ORDER_TYPES[stopOrderType],
3338                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3339                    ))
3340
3341                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3342                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3343                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3344                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3345                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3346                        ))
3347
3348                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3349                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3350                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3351                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3352                        ))
3353
3354            else:
3355                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3356
3357        return response
3358
3359    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3360        """
3361        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3362        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3363        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3364        See also: `Order()` docstring.
3365
3366        :param lots: volume, integer count of lots >= 1.
3367        :param targetPrice: target price > 0. This is open trade price for limit order.
3368        :return: JSON with response from broker server.
3369        """
3370        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3371
3372    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3373        """
3374        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3375        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3376        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3377        target price value then broker opens a limit order. See also: `Order()` docstring.
3378
3379        :param lots: volume, integer count of lots >= 1.
3380        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3381        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3382                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3383        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3384                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3385        :param expDate: string "Undefined" by default or local date in future.
3386                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3387                        This date is converting to UTC format for server.
3388        :return: JSON with response from broker server.
3389        """
3390        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3391
3392    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3393        """
3394        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3395        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3396        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3397        See also: `Order()` docstring.
3398
3399        :param lots: volume, integer count of lots >= 1.
3400        :param targetPrice: target price > 0. This is open trade price for limit order.
3401        :return: JSON with response from broker server.
3402        """
3403        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3404
3405    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3406        """
3407        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3408        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3409        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3410        target price value then broker opens a limit order. See also: `Order()` docstring.
3411
3412        :param lots: volume, integer count of lots >= 1.
3413        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3414        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3415                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3416        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3417                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3418        :param expDate: string "Undefined" by default or local date in future.
3419                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3420                        This date is converting to UTC format for server.
3421        :return: JSON with response from broker server.
3422        """
3423        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3424
3425    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3426        """
3427        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3428
3429        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3430        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3431                             This avoids unnecessary downloading data from the server.
3432        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3433        """
3434        if self.accountId is None or not self.accountId:
3435            uLogger.error("Variable `accountId` must be defined for using this method!")
3436            raise Exception("Account ID required")
3437
3438        if orderIDs:
3439            if allOrdersIDs is None:
3440                rawOrders = self.RequestPendingOrders()
3441                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3442
3443            if allStopOrdersIDs is None:
3444                rawStopOrders = self.RequestStopOrders()
3445                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3446
3447            for orderID in orderIDs:
3448                idInPendingOrders = orderID in allOrdersIDs
3449                idInStopOrders = orderID in allStopOrdersIDs
3450
3451                if not (idInPendingOrders or idInStopOrders):
3452                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3453                    continue
3454
3455                else:
3456                    if idInPendingOrders:
3457                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3458
3459                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3460                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3461                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3462                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3463
3464                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3465                            if self.moreDebug:
3466                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3467
3468                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3469
3470                        else:
3471                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3472
3473                    elif idInStopOrders:
3474                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3475
3476                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3477                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3478                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3479                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3480
3481                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3482                            if self.moreDebug:
3483                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3484
3485                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3486
3487                        else:
3488                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3489
3490                    else:
3491                        continue
3492
3493    def CloseAllOrders(self) -> None:
3494        """
3495        Gets a list of open pending and stop orders and cancel it all.
3496        """
3497        rawOrders = self.RequestPendingOrders()
3498        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3499        lenOrders = len(allOrdersIDs)
3500
3501        rawStopOrders = self.RequestStopOrders()
3502        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3503        lenSOrders = len(allStopOrdersIDs)
3504
3505        if lenOrders > 0 or lenSOrders > 0:
3506            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3507
3508            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3509
3510        else:
3511            uLogger.info("Orders not found, nothing to cancel.")
3512
3513    def CloseAll(self, *args) -> None:
3514        """
3515        Close all available (not blocked) opened trades and orders.
3516
3517        Also, you can select one or more keywords case-insensitive:
3518        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3519
3520        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3521        """
3522        overview = self.Overview(show=False)  # get all open trades info
3523
3524        if len(args) == 0:
3525            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3526            self.CloseAllOrders()  # close all pending and stop orders
3527
3528            for iType in TKS_INSTRUMENTS:
3529                if iType != "Currencies":
3530                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3531
3532        else:
3533            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3534            lowerArgs = [x.lower() for x in args]
3535
3536            if "orders" in lowerArgs:
3537                self.CloseAllOrders()  # close all pending and stop orders
3538
3539            for iType in TKS_INSTRUMENTS:
3540                if iType.lower() in lowerArgs and iType != "Currencies":
3541                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3542
3543    def CloseAllByTicker(self, instrument: str) -> None:
3544        """
3545        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3546
3547        This method searches opened trade and orders of instrument throw all portfolio and then use
3548        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3549
3550        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3551
3552        :param instrument: string with ticker.
3553        """
3554        if instrument is None or not instrument:
3555            uLogger.error("Ticker name must be defined for using this method!")
3556            raise Exception("Ticker required")
3557
3558        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3559
3560        self._ticker = instrument  # try to set instrument as ticker
3561        self._figi = ""
3562
3563        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3564        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3565
3566        if limitAll and self.IsInLimitOrders(portfolio=overview):
3567            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3568            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3569
3570        if stopAll and self.IsInStopOrders(portfolio=overview):
3571            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3572            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3573
3574        if self.IsInPortfolio(portfolio=overview):
3575            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3576            self.CloseTrades(instruments=[instrument], portfolio=overview)
3577
3578    def CloseAllByFIGI(self, instrument: str) -> None:
3579        """
3580        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3581
3582        This method searches opened trade and orders of instrument throw all portfolio and then use
3583        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3584
3585        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3586
3587        :param instrument: string with FIGI id.
3588        """
3589        if instrument is None or not instrument:
3590            uLogger.error("FIGI id must be defined for using this method!")
3591            raise Exception("FIGI required")
3592
3593        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3594
3595        self._ticker = ""
3596        self._figi = instrument  # try to set instrument as FIGI id
3597
3598        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3599        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3600
3601        if limitAll and self.IsInLimitOrders(portfolio=overview):
3602            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3603            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3604
3605        if stopAll and self.IsInStopOrders(portfolio=overview):
3606            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3607            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3608
3609        if self.IsInPortfolio(portfolio=overview):
3610            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3611            self.CloseTrades(instruments=[instrument], portfolio=overview)
3612
3613    @staticmethod
3614    def ParseOrderParameters(operation, **inputParameters):
3615        """
3616        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3617
3618        :param operation: string "Buy" or "Sell".
3619        :param inputParameters: this is dict of strings that looks like this
3620               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3621               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3622               "prices" key: one or more prices to open limit-orders
3623               Counts of values in lots and prices lists must be equals!
3624        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3625        """
3626        # TODO: update order grid work with api v2
3627        pass
3628        # uLogger.debug("Input parameters: {}".format(inputParameters))
3629        #
3630        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3631        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3632        #     raise Exception("Incorrect value")
3633        #
3634        # if "l" in inputParameters.keys():
3635        #     inputParameters["lots"] = inputParameters.pop("l")
3636        #
3637        # if "p" in inputParameters.keys():
3638        #     inputParameters["prices"] = inputParameters.pop("p")
3639        #
3640        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3641        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3642        #     raise Exception("Incorrect value")
3643        #
3644        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3645        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3646        #
3647        # if len(lots) != len(prices):
3648        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3649        #     raise Exception("Incorrect value")
3650        #
3651        # uLogger.debug("Extracted parameters for orders:")
3652        # uLogger.debug("lots = {}".format(lots))
3653        # uLogger.debug("prices = {}".format(prices))
3654        #
3655        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3656        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3657        # uLogger.debug("Order parameters: {}".format(result))
3658        #
3659        # return result
3660
3661    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3662        """
3663        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3664
3665        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3666        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3667        """
3668        result = False
3669        msg = "Instrument not defined!"
3670
3671        if portfolio is None or not portfolio:
3672            portfolio = self.Overview(show=False)
3673
3674        if self._ticker:
3675            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3676            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3677
3678            for iType in TKS_INSTRUMENTS:
3679                for instrument in portfolio["stat"][iType]:
3680                    if instrument["ticker"] == self._ticker:
3681                        result = True
3682                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3683                        break
3684
3685        elif self._figi:
3686            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3687            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3688
3689            for iType in TKS_INSTRUMENTS:
3690                for instrument in portfolio["stat"][iType]:
3691                    if instrument["figi"] == self._figi:
3692                        result = True
3693                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3694                        break
3695
3696        else:
3697            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3698
3699        uLogger.debug(msg)
3700
3701        return result
3702
3703    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3704        """
3705        Returns instrument from the user's portfolio if it presents there.
3706        Instrument must be defined by `ticker` (highly priority) or `figi`.
3707
3708        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3709        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3710        """
3711        result = None
3712        msg = "Instrument not defined!"
3713
3714        if portfolio is None or not portfolio:
3715            portfolio = self.Overview(show=False)
3716
3717        if self._ticker:
3718            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3719            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3720
3721            for iType in TKS_INSTRUMENTS:
3722                for instrument in portfolio["stat"][iType]:
3723                    if instrument["ticker"] == self._ticker:
3724                        result = instrument
3725                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3726                        break
3727
3728        elif self._figi:
3729            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3730            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3731
3732            for iType in TKS_INSTRUMENTS:
3733                for instrument in portfolio["stat"][iType]:
3734                    if instrument["figi"] == self._figi:
3735                        result = instrument
3736                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3737                        break
3738
3739        else:
3740            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3741
3742        uLogger.debug(msg)
3743
3744        return result
3745
3746    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3747        """
3748        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3749
3750        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3751
3752        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3753        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3754        """
3755        result = False
3756        msg = "Instrument not defined!"
3757
3758        if portfolio is None or not portfolio:
3759            portfolio = self.Overview(show=False)
3760
3761        if self._ticker:
3762            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3763            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3764
3765            for instrument in portfolio["stat"]["orders"]:
3766                if instrument["ticker"] == self._ticker:
3767                    result = True
3768                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3769                    break
3770
3771        elif self._figi:
3772            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3773            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3774
3775            for instrument in portfolio["stat"]["orders"]:
3776                if instrument["figi"] == self._figi:
3777                    result = True
3778                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3779                    break
3780
3781        else:
3782            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3783
3784        uLogger.debug(msg)
3785
3786        return result
3787
3788    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3789        """
3790        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3791        Instrument must be defined by `ticker` (highly priority) or `figi`.
3792
3793        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3794
3795        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3796        :return: list with `orderID`s of limit orders.
3797        """
3798        result = []
3799        msg = "Instrument not defined!"
3800
3801        if portfolio is None or not portfolio:
3802            portfolio = self.Overview(show=False)
3803
3804        if self._ticker:
3805            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3806            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3807
3808            for instrument in portfolio["stat"]["orders"]:
3809                if instrument["ticker"] == self._ticker:
3810                    result.append(instrument["orderID"])
3811
3812            if result:
3813                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3814
3815        elif self._figi:
3816            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3817            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3818
3819            for instrument in portfolio["stat"]["orders"]:
3820                if instrument["figi"] == self._figi:
3821                    result.append(instrument["orderID"])
3822
3823            if result:
3824                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3825
3826        else:
3827            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3828
3829        uLogger.debug(msg)
3830
3831        return result
3832
3833    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3834        """
3835        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3836
3837        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3838
3839        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3840        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3841        """
3842        result = False
3843        msg = "Instrument not defined!"
3844
3845        if portfolio is None or not portfolio:
3846            portfolio = self.Overview(show=False)
3847
3848        if self._ticker:
3849            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3850            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3851
3852            for instrument in portfolio["stat"]["stopOrders"]:
3853                if instrument["ticker"] == self._ticker:
3854                    result = True
3855                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3856                    break
3857
3858        elif self._figi:
3859            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3860            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3861
3862            for instrument in portfolio["stat"]["stopOrders"]:
3863                if instrument["figi"] == self._figi:
3864                    result = True
3865                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3866                    break
3867
3868        else:
3869            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3870
3871        uLogger.debug(msg)
3872
3873        return result
3874
3875    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3876        """
3877        Returns list with all `orderID`s of opened stop orders for the instrument.
3878        Instrument must be defined by `ticker` (highly priority) or `figi`.
3879
3880        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3881
3882        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3883        :return: list with `orderID`s of stop orders.
3884        """
3885        result = []
3886        msg = "Instrument not defined!"
3887
3888        if portfolio is None or not portfolio:
3889            portfolio = self.Overview(show=False)
3890
3891        if self._ticker:
3892            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3893            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3894
3895            for instrument in portfolio["stat"]["stopOrders"]:
3896                if instrument["ticker"] == self._ticker:
3897                    result.append(instrument["orderID"])
3898
3899            if result:
3900                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3901
3902        elif self._figi:
3903            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3904            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3905
3906            for instrument in portfolio["stat"]["stopOrders"]:
3907                if instrument["figi"] == self._figi:
3908                    result.append(instrument["orderID"])
3909
3910            if result:
3911                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3912
3913        else:
3914            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3915
3916        uLogger.debug(msg)
3917
3918        return result
3919
3920    def RequestLimits(self) -> dict:
3921        """
3922        Method for obtaining the available funds for withdrawal for current `accountId`.
3923
3924        See also:
3925        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3926        - `OverviewLimits()` method
3927
3928        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3929                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3930                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3931                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3932        """
3933        if self.accountId is None or not self.accountId:
3934            uLogger.error("Variable `accountId` must be defined for using this method!")
3935            raise Exception("Account ID required")
3936
3937        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3938
3939        self.body = str({"accountId": self.accountId})
3940        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3941        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3942
3943        if self.moreDebug:
3944            uLogger.debug("Records about available funds for withdrawal successfully received")
3945
3946        return rawLimits
3947
3948    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3949        """
3950        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3951
3952        See also: `RequestLimits()`.
3953
3954        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3955        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3956        :return: dict with raw parsed data from server and some calculated statistics about it.
3957        """
3958        if self.accountId is None or not self.accountId:
3959            uLogger.error("Variable `accountId` must be defined for using this method!")
3960            raise Exception("Account ID required")
3961
3962        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3963
3964        view = {
3965            "rawLimits": rawLimits,
3966            "limits": {  # parsed data for every currency:
3967                "money": {  # this is an array of portfolio currency positions
3968                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3969                },
3970                "blocked": {  # this is an array of blocked currency
3971                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3972                },
3973                "blockedGuarantee": {  # this is locked money under collateral for futures
3974                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3975                },
3976            },
3977        }
3978
3979        # --- Prepare text table with limits in human-readable format:
3980        if show or onlyFiles:
3981            info = [
3982                "# Withdrawal limits\n\n",
3983                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3984                "* **Account ID:** [{}]\n".format(self.accountId),
3985            ]
3986
3987            if view["limits"]["money"]:
3988                info.extend([
3989                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3990                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3991                ])
3992
3993            else:
3994                info.append("\nNo withdrawal limits\n")
3995
3996            for curr in view["limits"]["money"].keys():
3997                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3998                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3999                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
4000
4001                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
4002                    "[{}]".format(curr),
4003                    "{:.2f}".format(view["limits"]["money"][curr]),
4004                    "{:.2f}".format(availableMoney),
4005                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
4006                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
4007                )
4008
4009                if curr == "rub":
4010                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
4011
4012                else:
4013                    info.append(infoStr)
4014
4015            infoText = "".join(info)
4016
4017            if show and not onlyFiles:
4018                uLogger.info(infoText)
4019
4020            if self.withdrawalLimitsFile and (show or onlyFiles):
4021                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
4022                    fH.write(infoText)
4023
4024                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
4025
4026                if self.useHTMLReports:
4027                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
4028                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4029                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4030
4031                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4032
4033        return view
4034
4035    def RequestAccounts(self) -> dict:
4036        """
4037        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4038
4039        See also:
4040        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4041        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4042        - `OverviewUserInfo()` method
4043
4044        :return: dict with raw data from server that contains accounts info. Example of dict:
4045                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4046                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4047                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4048                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4049        """
4050        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4051
4052        self.body = str({})
4053        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4054        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4055
4056        if self.moreDebug:
4057            uLogger.debug("Records about available accounts successfully received")
4058
4059        return rawAccounts
4060
4061    def RequestUserInfo(self) -> dict:
4062        """
4063        Method for requesting common user's information.
4064
4065        See also:
4066        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4067        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4068        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4069        - `OverviewUserInfo()` method
4070
4071        :return: dict with raw data from server that contains user's information. Example of dict:
4072                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4073                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4074        """
4075        uLogger.debug("Requesting common user's information. Wait, please...")
4076
4077        self.body = str({})
4078        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4079        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4080
4081        if self.moreDebug:
4082            uLogger.debug("Records about current user successfully received")
4083
4084        return rawUserInfo
4085
4086    def RequestMarginStatus(self, accountId: str = None) -> dict:
4087        """
4088        Method for requesting margin calculation for defined account ID.
4089
4090        See also:
4091        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4092        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4093        - `OverviewUserInfo()` method
4094
4095        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4096        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4097                 Example of responses:
4098                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4099                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4100                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4101                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4102                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4103                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4104        """
4105        if accountId is None or not accountId:
4106            if self.accountId is None or not self.accountId:
4107                uLogger.error("Variable `accountId` must be defined for using this method!")
4108                raise Exception("Account ID required")
4109
4110            else:
4111                accountId = self.accountId  # use `self.accountId` (main ID) by default
4112
4113        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4114
4115        self.body = str({"accountId": accountId})
4116        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4117        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4118
4119        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4120            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4121            rawMargin = {}
4122
4123        else:
4124            if self.moreDebug:
4125                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4126
4127        return rawMargin
4128
4129    def RequestTariffLimits(self) -> dict:
4130        """
4131        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4132
4133        See also:
4134        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4135        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4136        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4137        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4138        - `OverviewUserInfo()` method
4139
4140        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4141                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4142                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4143        """
4144        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4145
4146        self.body = str({})
4147        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4148        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4149
4150        if self.moreDebug:
4151            uLogger.debug("Records with limits of current tariff successfully received")
4152
4153        return rawTariffLimits
4154
4155    def RequestBondCoupons(self, iJSON: dict) -> dict:
4156        """
4157        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4158        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4159        All dates are in UTC timezone.
4160
4161        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4162        Documentation:
4163        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4164        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4165
4166        See also: `ExtendBondsData()`.
4167
4168        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4169                      If raw iJSON is not data of bond then server returns an error [400] with message:
4170                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4171        :return: dictionary with bond payment calendar. Response example
4172                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4173                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4174                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4175                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4176        """
4177        if iJSON["figi"] is None or not iJSON["figi"]:
4178            uLogger.error("FIGI must be defined for using this method!")
4179            raise Exception("FIGI required")
4180
4181        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4182        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4183
4184        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4185            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4186            self._figi,
4187            startDate,
4188            endDate,
4189        ))
4190
4191        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4192        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4193        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4194
4195        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4196            uLogger.warning("Instrument type is not bond!")
4197
4198        else:
4199            if self.moreDebug:
4200                uLogger.debug("Records about bond payment calendar successfully received")
4201
4202        return calendar
4203
4204    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4205        """
4206        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4207        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4208        coupon yields, current yields and some statistics etc.
4209
4210        WARNING! This is too long operation if a lot of bonds requested from broker server.
4211
4212        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4213
4214        :param instruments: list of strings with tickers or FIGIs.
4215        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4216                     for further used by data scientists or stock analytics.
4217        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4218                 In XLSX-file and Pandas DataFrame fields mean:
4219                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4220                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4221        """
4222        if instruments is None or not instruments:
4223            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4224            raise Exception("Ticker or FIGI required")
4225
4226        if isinstance(instruments, str):
4227            instruments = [instruments]
4228
4229        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4230
4231        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4232
4233        iCount = len(uniqueInstruments)
4234        tooLong = iCount >= 20
4235        if tooLong:
4236            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4237
4238        bonds = None
4239        for i, self._figi in enumerate(uniqueInstruments):
4240            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4241
4242            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4243                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4244                rawBond = self.SearchByFIGI(requestPrice=True)
4245
4246                # Widen raw data with UTC current time (iData["actualDateTime"]):
4247                actualDate = datetime.now(tzutc())
4248                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4249
4250                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4251                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4252
4253                # Replace some values with human-readable:
4254                iData["nominalCurrency"] = iData["nominal"]["currency"]
4255                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4256                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4257                iData["aciCurrency"] = iData["aciValue"]["currency"]
4258                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4259                iData["issueSize"] = int(iData["issueSize"])
4260                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4261                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4262                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4263                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4264                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4265                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4266                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4267                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4268                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4269                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4270
4271                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4272                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4273                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4274                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4275                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4276                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4277                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4278                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4279                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4280                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4281                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4282
4283                # Widen raw data with calendar data from `rawCalendar` values:
4284                calendarData = []
4285                if "events" in iData["rawCalendar"].keys():
4286                    for item in iData["rawCalendar"]["events"]:
4287                        calendarData.append({
4288                            "couponDate": item["couponDate"],
4289                            "couponNumber": int(item["couponNumber"]),
4290                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4291                            "payCurrency": item["payOneBond"]["currency"],
4292                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4293                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4294                            "couponStartDate": item["couponStartDate"],
4295                            "couponEndDate": item["couponEndDate"],
4296                            "couponPeriod": item["couponPeriod"],
4297                        })
4298
4299                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4300                    if "maturityDate" not in iData.keys():
4301                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4302
4303                # Widen raw data with Coupon Rate.
4304                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4305                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4306                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4307                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4308
4309                # Widen raw data with Yield to Maturity (YTM) on current date.
4310                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4311                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4312                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4313                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4314                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4315                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4316
4317                iData["calendar"] = calendarData  # adds calendar at the end
4318
4319                # Remove not used data:
4320                iData.pop("uid")
4321                iData.pop("positionUid")
4322                iData.pop("currentPrice")
4323                iData.pop("rawCalendar")
4324
4325                colNames = list(iData.keys())
4326                if bonds is None:
4327                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4328
4329                else:
4330                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4331
4332            else:
4333                uLogger.warning("Instrument is not a bond!")
4334
4335            processed = round(100 * (i + 1) / iCount, 1)
4336            if tooLong and processed % 5 == 0:
4337                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4338
4339            else:
4340                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4341
4342        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4343
4344        # Saving bonds from Pandas DataFrame to XLSX sheet:
4345        if xlsx and self.bondsXLSXFile:
4346            with pd.ExcelWriter(
4347                    path=self.bondsXLSXFile,
4348                    date_format=TKS_DATE_FORMAT,
4349                    datetime_format=TKS_DATE_TIME_FORMAT,
4350                    mode="w",
4351            ) as writer:
4352                bonds.to_excel(
4353                    writer,
4354                    sheet_name="Extended bonds data",
4355                    index=True,
4356                    encoding="UTF-8",
4357                    freeze_panes=(1, 1),
4358                )  # saving as XLSX-file with freeze first row and column as headers
4359
4360            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4361
4362        return bonds
4363
4364    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4365        """
4366        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4367
4368        WARNING! This is too long operation if a lot of bonds requested from broker server.
4369
4370        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4371
4372        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4373                        extended information about bonds: main info, current prices, bond payment calendar,
4374                        coupon yields, current yields and some statistics etc.
4375                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4376        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4377                     for further used by data scientists or stock analytics.
4378        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4379        """
4380        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4381            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4382
4383        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4384
4385        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4386        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4387        calendar = None
4388        for bond in extBonds.iterrows():
4389            for item in bond[1]["calendar"]:
4390                cData = {
4391                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4392                    "couponDate": item["couponDate"],
4393                    "figi": bond[1]["figi"],
4394                    "ticker": bond[1]["ticker"],
4395                    "name": bond[1]["name"],
4396                    "couponNumber": item["couponNumber"],
4397                    "payOneBond": item["payOneBond"],
4398                    "payCurrency": item["payCurrency"],
4399                    "couponType": item["couponType"],
4400                    "couponPeriod": item["couponPeriod"],
4401                    "fixDate": item["fixDate"],
4402                    "couponStartDate": item["couponStartDate"],
4403                    "couponEndDate": item["couponEndDate"],
4404                }
4405
4406                if calendar is None:
4407                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4408
4409                else:
4410                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4411
4412        if calendar is not None:
4413            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4414
4415            # Saving calendar from Pandas DataFrame to XLSX sheet:
4416            if xlsx:
4417                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4418
4419                with pd.ExcelWriter(
4420                        path=xlsxCalendarFile,
4421                        date_format=TKS_DATE_FORMAT,
4422                        datetime_format=TKS_DATE_TIME_FORMAT,
4423                        mode="w",
4424                ) as writer:
4425                    humanReadable = calendar.copy(deep=True)
4426                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4427                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4428                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4429                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4430                    humanReadable.columns = colNames  # human-readable column names
4431
4432                    humanReadable.to_excel(
4433                        writer,
4434                        sheet_name="Bond payments calendar",
4435                        index=False,
4436                        encoding="UTF-8",
4437                        freeze_panes=(1, 2),
4438                    )  # saving as XLSX-file with freeze first row and column as headers
4439
4440                    del humanReadable  # release df in memory
4441
4442                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4443
4444        return calendar
4445
4446    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4447        """
4448        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4449        Also, creates Markdown file with calendar data, `calendar.md` by default.
4450
4451        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4452
4453        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4454                        extended information about bonds: main info, current prices, bond payment calendar,
4455                        coupon yields, current yields and some statistics etc.
4456                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4457        :param show: if `True` then also printing bonds payment calendar to the console,
4458                     otherwise save to file `calendarFile` only. `False` by default.
4459        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4460        :return: multilines text in Markdown format with bonds payment calendar as a table.
4461        """
4462        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4463            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4464
4465        infoText = "# Bond payments calendar\n\n"
4466
4467        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4468
4469        if not (calendar is None or calendar.empty):
4470            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4471
4472            info = [
4473                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4474                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4475                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4476            ]
4477
4478            newMonth = False
4479            notOneBond = calendar["figi"].nunique() > 1
4480            for i, bond in enumerate(calendar.iterrows()):
4481                if newMonth and notOneBond:
4482                    info.append(splitLine)
4483
4484                info.append(
4485                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4486                        "  √" if bond[1]["paid"] else "  —",
4487                        bond[1]["couponDate"].split("T")[0],
4488                        bond[1]["figi"],
4489                        bond[1]["ticker"],
4490                        bond[1]["couponNumber"],
4491                        "{} {}".format(
4492                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4493                            bond[1]["payCurrency"],
4494                        ),
4495                        bond[1]["couponType"],
4496                        bond[1]["couponPeriod"],
4497                        bond[1]["fixDate"].split("T")[0],
4498                    )
4499                )
4500
4501                if i < len(calendar.values) - 1:
4502                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4503                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4504                    newMonth = False if curDate.month == nextDate.month else True
4505
4506                else:
4507                    newMonth = False
4508
4509            infoText += "".join(info)
4510
4511            if show and not onlyFiles:
4512                uLogger.info("{}".format(infoText))
4513
4514            if self.calendarFile is not None and (show or onlyFiles):
4515                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4516                    fH.write(infoText)
4517
4518                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4519
4520                if self.useHTMLReports:
4521                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4522                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4523                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4524
4525                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4526
4527        else:
4528            infoText += "No data\n"
4529
4530        return infoText
4531
4532    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4533        """
4534        Method for parsing and show simple table with all available user accounts.
4535
4536        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4537
4538        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4539        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4540        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4541                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4542                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4543                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4544                                                        "closed": "—", "access": "Full access" }, ...}}`
4545        """
4546        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4547
4548        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4549        accounts = {
4550            item["id"]: {
4551                "type": TKS_ACCOUNT_TYPES[item["type"]],
4552                "name": item["name"],
4553                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4554                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4555                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4556                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4557            } for item in rawAccounts["accounts"]
4558        }
4559
4560        # Raw and parsed data with some fields replaced in "stat" section:
4561        view = {
4562            "rawAccounts": rawAccounts,
4563            "stat": accounts,
4564        }
4565
4566        # --- Prepare simple text table with only accounts data in human-readable format:
4567        if show or onlyFiles:
4568            info = [
4569                "# User accounts\n\n",
4570                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4571                "| Account ID   | Type                      | Status                    | Name                           |\n",
4572                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4573            ]
4574
4575            for account in view["stat"].keys():
4576                info.extend([
4577                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4578                        account,
4579                        view["stat"][account]["type"],
4580                        view["stat"][account]["status"],
4581                        view["stat"][account]["name"],
4582                    )
4583                ])
4584
4585            infoText = "".join(info)
4586
4587            if show and not onlyFiles:
4588                uLogger.info(infoText)
4589
4590            if self.userAccountsFile and (show or onlyFiles):
4591                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4592                    fH.write(infoText)
4593
4594                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4595
4596                if self.useHTMLReports:
4597                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4598                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4599                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4600
4601                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4602
4603        return view
4604
4605    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4606        """
4607        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4608
4609        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4610
4611        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4612        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4613        :return: dict with raw parsed data from server and some calculated statistics about it.
4614        """
4615        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4616        tmpTicker = self._ticker
4617        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4618        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4619        self._ticker = tmpTicker
4620
4621        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4622        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4623        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4624        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4625        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4626        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4627
4628        # This is dict with parsed common user data:
4629        userInfo = {
4630            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4631            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4632            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4633            "tariff": rawUserInfo["tariff"],
4634        }
4635
4636        # This is an array of dict with parsed margin statuses for every account IDs:
4637        margins = {}
4638        for accountId in accounts.keys():
4639            if rawMargins[accountId]:
4640                margins[accountId] = {
4641                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4642                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4643                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4644                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4645                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4646                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4647                    "missing": missing["volume"],
4648                }
4649
4650            else:
4651                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4652
4653        unary = {}  # unary-connection limits
4654        for item in rawTariffLimits["unaryLimits"]:
4655            if item["limitPerMinute"] in unary.keys():
4656                unary[item["limitPerMinute"]].extend(item["methods"])
4657
4658            else:
4659                unary[item["limitPerMinute"]] = item["methods"]
4660
4661        stream = {}  # stream-connection limits
4662        for item in rawTariffLimits["streamLimits"]:
4663            if item["limit"] in stream.keys():
4664                stream[item["limit"]].extend(item["streams"])
4665
4666            else:
4667                stream[item["limit"]] = item["streams"]
4668
4669        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4670        limits = {
4671            "unary": unary,
4672            "stream": stream,
4673        }
4674
4675        # Raw and parsed data as an output result:
4676        view = {
4677            "rawUserInfo": rawUserInfo,
4678            "rawAccounts": rawAccounts,
4679            "rawMargins": rawMargins,
4680            "rawTariffLimits": rawTariffLimits,
4681            "stat": {
4682                "overview": overview,
4683                "userInfo": userInfo,
4684                "accounts": accounts,
4685                "margins": margins,
4686                "limits": limits,
4687            },
4688        }
4689
4690        # --- Prepare text table with user information in human-readable format:
4691        if show or onlyFiles:
4692            info = [
4693                "# Full user information\n\n",
4694                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4695                "## Common information\n\n",
4696                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4697                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4698                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4699                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4700                "\n## User accounts\n\n",
4701            ]
4702
4703            for account in view["stat"]["accounts"].keys():
4704                info.extend([
4705                    "### ID: [{}]\n\n".format(account),
4706                    "| Parameters           | Values                                                       |\n",
4707                    "|----------------------|--------------------------------------------------------------|\n",
4708                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4709                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4710                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4711                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4712                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4713                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4714                ])
4715
4716                if margins[account]:
4717                    info.extend([
4718                        "| Margin status:       | Enabled                                                      |\n",
4719                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4720                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4721                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4722                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4723                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4724                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4725                    ])
4726
4727                else:
4728                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4729
4730            info.extend([
4731                "\n## Current user tariff limits\n",
4732                "\n### See also\n",
4733                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4734                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4735                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4736                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4737                "\n### Unary limits\n",
4738            ])
4739
4740            if unary:
4741                for key, values in sorted(unary.items()):
4742                    info.append("\n* Max requests per minute: {}\n".format(key))
4743
4744                    for value in values:
4745                        info.append("  - {}\n".format(value))
4746
4747            else:
4748                info.append("\nNot available\n")
4749
4750            info.append("\n### Stream limits\n")
4751
4752            if stream:
4753                for key, values in sorted(stream.items()):
4754                    info.append("\n* Max stream connections: {}\n".format(key))
4755
4756                    for value in values:
4757                        info.append("  - {}\n".format(value))
4758
4759            else:
4760                info.append("\nNot available\n")
4761
4762            infoText = "".join(info)
4763
4764            if show and not onlyFiles:
4765                uLogger.info(infoText)
4766
4767            if self.userInfoFile and (show or onlyFiles):
4768                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4769                    fH.write(infoText)
4770
4771                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4772
4773                if self.useHTMLReports:
4774                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4775                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4776                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4777
4778                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4779
4780        return view
4781
4782
4783class Args:
4784    """
4785    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4786    """
4787    def __init__(self, **kwargs):
4788        self.__dict__.update(kwargs)
4789
4790    def __getattr__(self, item):
4791        return None
4792
4793
4794def ParseArgs():
4795    """This function get and parse command line keys."""
4796    parser = ArgumentParser()  # command-line string parser
4797
4798    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4799    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4800
4801    # --- options:
4802
4803    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4804    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4805    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4806
4807    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4808    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4809
4810    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4811    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4812
4813    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4814    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4815
4816    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4817    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4818    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4819
4820    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4821    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4822    parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).")
4823
4824    # --- commands:
4825
4826    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4827
4828    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4829    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4830    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4831    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4832    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4833    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4834    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4835    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4836
4837    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4838    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4839    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4840    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4841    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4842    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4843
4844    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4845    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4846    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4847    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4848
4849    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4850    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4851    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4852
4853    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4854    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4855    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4856    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4857    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4858    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4859    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4860
4861    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4862    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4863    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4864    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4865    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4866
4867    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4868    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4869    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4870
4871    cmdArgs = parser.parse_args()
4872    return cmdArgs
4873
4874
4875def Main(**kwargs):
4876    """
4877    Main function for work with TKSBrokerAPI in the console.
4878
4879    See examples:
4880    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4881    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4882    """
4883    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4884
4885    if args.debug_level:
4886        uLogger.level = 10  # always debug level by default
4887        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4888
4889    exitCode = 0
4890    start = datetime.now(tzutc())
4891    uLogger.debug("=-" * 50)
4892    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4893        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4894        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4895    ))
4896
4897    # trying to calculate full current version:
4898    buildVersion = __version__
4899    try:
4900        v = version("tksbrokerapi")
4901        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4902
4903    except Exception:
4904        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4905
4906    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4907    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4908
4909    try:
4910        if args.version:
4911            print("TKSBrokerAPI {}".format(buildVersion))
4912            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4913
4914        else:
4915            # Init class for trading with Tinkoff Broker:
4916            trader = TinkoffBrokerServer(
4917                token=args.token,
4918                accountId=args.account_id,
4919                useCache=not args.no_cache,
4920            )
4921
4922            if args.tag is not None:
4923                trader.tag = args.tag  # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode
4924
4925            # --- set some options:
4926
4927            if args.more:
4928                trader.moreDebug = True
4929                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4930
4931            if args.html:
4932                trader.useHTMLReports = True
4933
4934            if args.ticker:
4935                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4936
4937                if ticker in trader.aliasesKeys:
4938                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4939
4940                else:
4941                    trader.ticker = ticker
4942
4943            if args.figi:
4944                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4945
4946            if args.depth is not None:
4947                trader.depth = args.depth
4948
4949            # --- do one command:
4950
4951            if args.list:
4952                if args.output is not None:
4953                    trader.instrumentsFile = args.output
4954
4955                trader.ShowInstrumentsInfo(show=True)
4956
4957            elif args.list_xlsx:
4958                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4959
4960            elif args.bonds_xlsx is not None:
4961                if args.output is not None:
4962                    trader.bondsXLSXFile = args.output
4963
4964                if len(args.bonds_xlsx) == 0:
4965                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4966
4967                else:
4968                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4969
4970            elif args.search:
4971                if args.output is not None:
4972                    trader.searchResultsFile = args.output
4973
4974                trader.SearchInstruments(pattern=args.search[0], show=True)
4975
4976            elif args.info:
4977                if not (args.ticker or args.figi):
4978                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4979                    raise Exception("Ticker or FIGI required")
4980
4981                if args.output is not None:
4982                    trader.infoFile = args.output
4983
4984                if args.ticker:
4985                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4986
4987                else:
4988                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4989
4990            elif args.calendar is not None:
4991                if args.output is not None:
4992                    trader.calendarFile = args.output
4993
4994                if len(args.calendar) == 0:
4995                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4996
4997                else:
4998                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4999
5000                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
5001
5002            elif args.price:
5003                if not (args.ticker or args.figi):
5004                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5005                    raise Exception("Ticker or FIGI required")
5006
5007                trader.GetCurrentPrices(show=True)
5008
5009            elif args.prices is not None:
5010                if args.output is not None:
5011                    trader.pricesFile = args.output
5012
5013                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
5014
5015            elif args.overview:
5016                if args.output is not None:
5017                    trader.overviewFile = args.output
5018
5019                trader.Overview(show=True, details="full")
5020
5021            elif args.overview_digest:
5022                if args.output is not None:
5023                    trader.overviewDigestFile = args.output
5024
5025                trader.Overview(show=True, details="digest")
5026
5027            elif args.overview_positions:
5028                if args.output is not None:
5029                    trader.overviewPositionsFile = args.output
5030
5031                trader.Overview(show=True, details="positions")
5032
5033            elif args.overview_orders:
5034                if args.output is not None:
5035                    trader.overviewOrdersFile = args.output
5036
5037                trader.Overview(show=True, details="orders")
5038
5039            elif args.overview_analytics:
5040                if args.output is not None:
5041                    trader.overviewAnalyticsFile = args.output
5042
5043                trader.Overview(show=True, details="analytics")
5044
5045            elif args.overview_calendar:
5046                if args.output is not None:
5047                    trader.overviewAnalyticsFile = args.output
5048
5049                trader.Overview(show=True, details="calendar")
5050
5051            elif args.deals is not None:
5052                if args.output is not None:
5053                    trader.reportFile = args.output
5054
5055                if 0 <= len(args.deals) < 3:
5056                    trader.Deals(
5057                        start=args.deals[0] if len(args.deals) >= 1 else None,
5058                        end=args.deals[1] if len(args.deals) == 2 else None,
5059                        show=True,  # Always show deals report in console
5060                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5061                    )
5062
5063                else:
5064                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5065                    raise Exception("Incorrect value")
5066
5067            elif args.history is not None:
5068                if args.output is not None:
5069                    trader.historyFile = args.output
5070
5071                if 0 <= len(args.history) < 3:
5072                    dataReceived = trader.History(
5073                        start=args.history[0] if len(args.history) >= 1 else None,
5074                        end=args.history[1] if len(args.history) == 2 else None,
5075                        interval="hour" if args.interval is None or not args.interval else args.interval,
5076                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5077                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5078                        show=True,  # shows all downloaded candles in console
5079                    )
5080
5081                    if args.render_chart is not None and dataReceived is not None:
5082                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5083
5084                        trader.ShowHistoryChart(
5085                            candles=dataReceived,
5086                            interact=iChart,
5087                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5088                        )
5089
5090                else:
5091                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5092                    raise Exception("Incorrect value")
5093
5094            elif args.load_history is not None:
5095                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5096
5097                if args.render_chart is not None and histData is not None:
5098                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5099                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5100
5101                    trader.ShowHistoryChart(
5102                        candles=histData,
5103                        interact=iChart,
5104                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5105                    )
5106
5107            elif args.trade is not None:
5108                if 1 <= len(args.trade) <= 5:
5109                    trader.Trade(
5110                        operation=args.trade[0],
5111                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5112                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5113                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5114                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5115                    )
5116
5117                else:
5118                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5119
5120            elif args.buy is not None:
5121                if 0 <= len(args.buy) <= 4:
5122                    trader.Buy(
5123                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5124                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5125                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5126                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5127                    )
5128
5129                else:
5130                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5131
5132            elif args.sell is not None:
5133                if 0 <= len(args.sell) <= 4:
5134                    trader.Sell(
5135                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5136                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5137                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5138                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5139                    )
5140
5141                else:
5142                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5143
5144            elif args.order:
5145                if 4 <= len(args.order) <= 7:
5146                    trader.Order(
5147                        operation=args.order[0],
5148                        orderType=args.order[1],
5149                        lots=int(args.order[2]),
5150                        targetPrice=float(args.order[3]),
5151                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5152                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5153                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5154                    )
5155
5156                else:
5157                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5158
5159            elif args.buy_limit:
5160                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5161
5162            elif args.sell_limit:
5163                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5164
5165            elif args.buy_stop:
5166                if 2 <= len(args.buy_stop) <= 7:
5167                    trader.BuyStop(
5168                        lots=int(args.buy_stop[0]),
5169                        targetPrice=float(args.buy_stop[1]),
5170                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5171                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5172                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5173                    )
5174
5175                else:
5176                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5177
5178            elif args.sell_stop:
5179                if 2 <= len(args.sell_stop) <= 7:
5180                    trader.SellStop(
5181                        lots=int(args.sell_stop[0]),
5182                        targetPrice=float(args.sell_stop[1]),
5183                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5184                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5185                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5186                    )
5187
5188                else:
5189                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5190
5191            # elif args.buy_order_grid is not None:
5192            #     # update order grid work with api v2
5193            #     if len(args.buy_order_grid) == 2:
5194            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5195            #
5196            #         for order in orderParams:
5197            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5198            #
5199            #     else:
5200            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5201            #
5202            # elif args.sell_order_grid is not None:
5203            #     # update order grid work with api v2
5204            #     if len(args.sell_order_grid) >= 2:
5205            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5206            #
5207            #         for order in orderParams:
5208            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5209            #
5210            #     else:
5211            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5212
5213            elif args.close_order is not None:
5214                trader.CloseOrders(args.close_order)  # close only one order
5215
5216            elif args.close_orders is not None:
5217                trader.CloseOrders(args.close_orders)  # close list of orders
5218
5219            elif args.close_trade:
5220                if not (args.ticker or args.figi):
5221                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5222                    raise Exception("Ticker or FIGI required")
5223
5224                if args.ticker:
5225                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5226
5227                else:
5228                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5229
5230            elif args.close_trades is not None:
5231                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5232
5233            elif args.close_all is not None:
5234                if args.ticker:
5235                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5236
5237                elif args.figi:
5238                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5239
5240                else:
5241                    trader.CloseAll(*args.close_all)
5242
5243            elif args.limits:
5244                if args.output is not None:
5245                    trader.withdrawalLimitsFile = args.output
5246
5247                trader.OverviewLimits(show=True)
5248
5249            elif args.user_info:
5250                if args.output is not None:
5251                    trader.userInfoFile = args.output
5252
5253                trader.OverviewUserInfo(show=True)
5254
5255            elif args.account:
5256                if args.output is not None:
5257                    trader.userAccountsFile = args.output
5258
5259                trader.OverviewAccounts(show=True)
5260
5261            else:
5262                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5263                raise Exception("There is no command to execute")
5264
5265    except Exception:
5266        trace = tb.format_exc()
5267        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5268            if e in trace:
5269                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5270                break
5271
5272        uLogger.debug(trace)
5273        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5274        exitCode = 255  # an error occurred, must be open a ticket for this issue
5275
5276    finally:
5277        finish = datetime.now(tzutc())
5278
5279        if exitCode == 0:
5280            if args.more:
5281                uLogger.debug("All operations were finished success (summary code is 0).")
5282
5283        else:
5284            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5285                os.path.abspath(uLog.defaultLogFile), exitCode,
5286            ))
5287
5288        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5289        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5290            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5291            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5292        ))
5293        uLogger.debug("=-" * 50)
5294
5295        if not kwargs:
5296            sys.exit(exitCode)
5297
5298        else:
5299            return exitCode
5300
5301
5302if __name__ == "__main__":
5303    Main()
class TinkoffBrokerServer:
  80class TinkoffBrokerServer:
  81    """
  82    This class implements methods to work with Tinkoff broker server.
  83
  84    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  85
  86    About `token`: https://tinkoff.github.io/investAPI/token/
  87    """
  88    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  89        """
  90        Main class init.
  91
  92        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  93        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  94                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  95        :param useCache: use default cache file with raw data to use instead of `iList`.
  96                         True by default. Cache is auto-update if new day has come.
  97                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  98        :param defaultCache: path to default cache file. `dump.json` by default.
  99        """
 100        if token is None or not token:
 101            try:
 102                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 103                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 104
 105            except KeyError:
 106                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 107                raise Exception("Token required")
 108
 109        else:
 110            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 111            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 112
 113        if accountId is None or not accountId:
 114            try:
 115                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 116                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 117
 118            except KeyError:
 119                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 120
 121        else:
 122            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 123            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 124
 125        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 126        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 127
 128        Latest version: https://pypi.org/project/tksbrokerapi/
 129        """
 130
 131        self._tag = ""
 132        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 133
 134        self.__lock = Lock()  # initialize multiprocessing mutex lock
 135
 136        self._precision = 4  # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file
 137
 138        self.aliases = TKS_TICKER_ALIASES
 139        """Some aliases instead official tickers.
 140
 141        See also: `TKSEnums.TKS_TICKER_ALIASES`
 142        """
 143
 144        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 145
 146        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 147
 148        self._ticker = ""
 149        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 150
 151        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 152        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 153
 154        See also: `SearchByTicker()`, `SearchInstruments()`.
 155        """
 156
 157        self._figi = ""
 158        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 159
 160        See also: `SearchByFIGI()`, `SearchInstruments()`.
 161        """
 162
 163        self.depth = 1
 164        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 165
 166        See also: `GetCurrentPrices()`.
 167        """
 168
 169        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 170        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 171
 172        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 173        """
 174
 175        uLogger.debug("Broker API server: {}".format(self.server))
 176
 177        self.timeout = 15
 178        """Server operations timeout in seconds. Default: `15`.
 179
 180        See also: `SendAPIRequest()`.
 181        """
 182
 183        self.headers = {
 184            "Content-Type": "application/json",
 185            "accept": "application/json",
 186            "Authorization": "Bearer {}".format(self.token),
 187            "x-app-name": "Tim55667757.TKSBrokerAPI",
 188        }
 189        """
 190        Headers which send in every request to broker server. Please, do not change it!
 191        Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`.
 192
 193        See also: `SendAPIRequest()`.
 194        """
 195
 196        self.body = None
 197        """Request body which send to broker server. Default: `None`.
 198
 199        See also: `SendAPIRequest()`.
 200        """
 201
 202        self.moreDebug = False
 203        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 204
 205        self.useHTMLReports = False
 206        """
 207        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 208        
 209        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 210        """
 211
 212        self.historyFile = None
 213        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 214
 215        See also: `History()`.
 216        """
 217
 218        self.htmlHistoryFile = "index.html"
 219        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 220
 221        See also: `ShowHistoryChart()`.
 222        """
 223
 224        self.instrumentsFile = "instruments.md"
 225        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 226
 227        See also: `ShowInstrumentsInfo()`.
 228        """
 229
 230        self.searchResultsFile = "search-results.md"
 231        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 232
 233        See also: `SearchInstruments()`.
 234        """
 235
 236        self.pricesFile = "prices.md"
 237        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 238
 239        See also: `GetListOfPrices()`.
 240        """
 241
 242        self.infoFile = "info.md"
 243        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 244
 245        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 246        """
 247
 248        self.bondsXLSXFile = "ext-bonds.xlsx"
 249        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 250        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 251
 252        See also: `ExtendBondsData()`.
 253        """
 254
 255        self.calendarFile = "calendar.md"
 256        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 257        
 258        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 259
 260        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 261        """
 262
 263        self.overviewFile = "overview.md"
 264        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 265
 266        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 267        """
 268
 269        self.overviewDigestFile = "overview-digest.md"
 270        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 271
 272        See also: `Overview()` with parameter `details="digest"`.
 273        """
 274
 275        self.overviewPositionsFile = "overview-positions.md"
 276        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 277
 278        See also: `Overview()` with parameter `details="positions"`.
 279        """
 280
 281        self.overviewOrdersFile = "overview-orders.md"
 282        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 283
 284        See also: `Overview()` with parameter `details="orders"`.
 285        """
 286
 287        self.overviewAnalyticsFile = "overview-analytics.md"
 288        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 289
 290        See also: `Overview()` with parameter `details="analytics"`.
 291        """
 292
 293        self.overviewBondsCalendarFile = "overview-calendar.md"
 294        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 295
 296        See also: `Overview()` with parameter `details="calendar"`.
 297        """
 298
 299        self.reportFile = "deals.md"
 300        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 301
 302        See also: `Deals()`.
 303        """
 304
 305        self.withdrawalLimitsFile = "limits.md"
 306        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 307
 308        See also: `OverviewLimits()` and `RequestLimits()`.
 309        """
 310
 311        self.userInfoFile = "user-info.md"
 312        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 313
 314        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 315        """
 316
 317        self.userAccountsFile = "accounts.md"
 318        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 319
 320        See also: `OverviewAccounts()`, `RequestAccounts()`.
 321        """
 322
 323        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 324        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 325
 326        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 327
 328        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 329        """
 330
 331        self.iList = None  # init iList for raw instruments data
 332        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 333        
 334        See also: `Listing()`, `DumpInstruments()`.
 335        """
 336
 337        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 338        if useCache:
 339            if os.path.exists(self.iListDumpFile):
 340                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 341                curTime = datetime.now(tzutc())
 342
 343                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 344                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 345
 346                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 347
 348                else:
 349                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 350
 351                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 352                        os.path.abspath(self.iListDumpFile),
 353                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 354                    ))
 355
 356            else:
 357                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 358                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 359
 360        else:
 361            self.iList = self.Listing()  # request new raw instruments data from broker server
 362            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 363
 364        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 365        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 366
 367        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 368        """
 369
 370    @property
 371    def tag(self) -> str:
 372        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 373        return self._tag
 374
 375    @tag.setter
 376    def tag(self, value):
 377        """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 378        self._tag = str(value)
 379
 380        if self._tag:
 381            for handler in uLogger.handlers:
 382                handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag)))
 383
 384            uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag))
 385
 386        else:
 387            for handler in uLogger.handlers:
 388                handler.setFormatter(uLog.logging.Formatter(uLog.formatString))
 389
 390            uLogger.debug("Default logger format is used")
 391
 392    @property
 393    def ticker(self) -> str:
 394        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 395
 396        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 397        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 398
 399        See also: `SearchByTicker()`, `SearchInstruments()`.
 400        """
 401        return self._ticker
 402
 403    @ticker.setter
 404    def ticker(self, value):
 405        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 406
 407        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 408        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 409
 410        See also: `SearchByTicker()`, `SearchInstruments()`.
 411        """
 412        self._ticker = str(value).upper()  # Tickers may be upper case only
 413
 414    @property
 415    def figi(self) -> str:
 416        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 417
 418        See also: `SearchByFIGI()`, `SearchInstruments()`.
 419        """
 420        return self._figi
 421
 422    @figi.setter
 423    def figi(self, value):
 424        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 425
 426        See also: `SearchByFIGI()`, `SearchInstruments()`.
 427        """
 428        self._figi = str(value).upper()  # FIGI may be upper case only
 429
 430    def _ParseJSON(self, rawData="{}") -> dict:
 431        """
 432        Parse JSON from response string.
 433
 434        :param rawData: this is a string with JSON-formatted text.
 435        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 436        """
 437        try:
 438            responseJSON = json.loads(rawData) if rawData else {}
 439
 440            if self.moreDebug:
 441                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 442
 443            return responseJSON
 444
 445        except Exception as e:
 446            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 447
 448            return {}
 449
 450    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 451        """
 452        Send GET or POST request to broker server and receive JSON object.
 453
 454        self.header: must be defining with dictionary of headers.
 455        self.body: if define then used as request body. None by default.
 456        self.timeout: global request timeout, 15 seconds by default.
 457        :param url: url with REST request.
 458        :param reqType: send "GET" or "POST" request. "GET" by default.
 459        :param retry: how many times retry after first request if an 5xx server errors occurred.
 460        :param pause: sleep time in seconds between retries.
 461        :return: response JSON (dictionary) from broker.
 462        """
 463        if reqType.upper() not in ("GET", "POST"):
 464            uLogger.error("You can define request type: `GET` or `POST`!")
 465            raise Exception("Incorrect value")
 466
 467        if self.moreDebug:
 468            uLogger.debug("Request parameters:")
 469            uLogger.debug("    - REST API URL: {}".format(url))
 470            uLogger.debug("    - request type: {}".format(reqType))
 471            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 472            uLogger.debug("    - body:\n{}".format(self.body))
 473
 474        # fast hack to avoid all operations with some tickers/FIGI
 475        responseJSON = {}
 476        oK = True
 477        for item in self.exclude:
 478            if item in url:
 479                if self.moreDebug:
 480                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 481
 482                oK = False
 483                break
 484
 485        if oK:
 486            with self.__lock:  # acquire the mutex lock
 487                counter = 0
 488                response = None
 489                errMsg = ""
 490
 491                while not response and counter <= retry:
 492                    if reqType == "GET":
 493                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 494
 495                    if reqType == "POST":
 496                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 497
 498                    if self.moreDebug:
 499                        uLogger.debug("Response:")
 500                        uLogger.debug("    - status code: {}".format(response.status_code))
 501                        uLogger.debug("    - reason: {}".format(response.reason))
 502                        uLogger.debug("    - body length: {}".format(len(response.text)))
 503                        uLogger.debug("    - headers:\n{}".format(response.headers))
 504
 505                    # Server returns some headers:
 506                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 507                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 508                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 509                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 510                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 511                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 512                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 513                        sleep(rateLimitWait)
 514
 515                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 516                    if 400 <= response.status_code < 500:
 517                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 518                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 519
 520                        if "code" in response.text and "message" in response.text:
 521                            msgDict = self._ParseJSON(rawData=response.text)
 522                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 523
 524                        counter = retry + 1  # do not retry for 4xx errors
 525
 526                    if 500 <= response.status_code < 600:
 527                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 528                        uLogger.debug("    - not oK, {}".format(errMsg))
 529
 530                        if "code" in response.text and "message" in response.text:
 531                            errMsgDict = self._ParseJSON(rawData=response.text)
 532                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 533
 534                        counter += 1
 535
 536                        if counter <= retry:
 537                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 538                            sleep(pause)
 539
 540                responseJSON = self._ParseJSON(rawData=response.text)
 541
 542                if errMsg:
 543                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 544                    uLogger.error("    - not oK, {}".format(errMsg))
 545
 546        return responseJSON
 547
 548    def _IUpdater(self, iType: str) -> tuple:
 549        """
 550        Request instrument by type from server. See available API methods for instruments:
 551        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 552        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 553        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 554        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 555        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 556
 557        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 558        :return: tuple with iType name and list of available instruments of current type for defined user token.
 559        """
 560        result = []
 561
 562        if iType in TKS_INSTRUMENTS:
 563            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 564
 565            # all instruments have the same body in API v2 requests:
 566            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 567            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 568            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 569
 570        return iType, result
 571
 572    def _IWrapper(self, kwargs):
 573        """
 574        Wrapper runs instrument's update method `_IUpdater()`.
 575        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 576        """
 577        return self._IUpdater(**kwargs)
 578
 579    def Listing(self) -> dict:
 580        """
 581        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 582
 583        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 584        """
 585        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 586        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 587
 588        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 589        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 590        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 591
 592        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 593        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 594        poolUpdater.close()  # close the thread pool
 595        poolUpdater.join()  # wait a moment until all data returns from threads
 596
 597        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 598        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 599        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 600
 601        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 602        for iType in iList.keys():
 603            for ticker in iList[iType]:
 604                iList[iType][ticker]["type"] = iType
 605
 606                if "minPriceIncrement" in iList[iType][ticker].keys():
 607                    iList[iType][ticker]["step"] = NanoToFloat(
 608                        iList[iType][ticker]["minPriceIncrement"]["units"],
 609                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 610                    )
 611
 612                else:
 613                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 614
 615        return iList
 616
 617    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 618        """
 619        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 620
 621        See also: `DumpInstruments()`, `Listing()`.
 622
 623        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 624                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 625        """
 626        if self.iListDumpFile is None or not self.iListDumpFile:
 627            uLogger.error("Output name of dump file must be defined!")
 628            raise Exception("Filename required")
 629
 630        if not self.iList or forceUpdate:
 631            self.iList = self.Listing()
 632
 633        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 634
 635        # Save as XLSX with separated sheets for every type of instruments:
 636        with pd.ExcelWriter(
 637                path=xlsxDumpFile,
 638                date_format=TKS_DATE_FORMAT,
 639                datetime_format=TKS_DATE_TIME_FORMAT,
 640                mode="w",
 641        ) as writer:
 642            for iType in TKS_INSTRUMENTS:
 643                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 644                df = df[sorted(df)]  # sorted by column names
 645                df = df.applymap(
 646                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 647                    na_action="ignore",
 648                )  # converting numbers from nano-type to float in every cell
 649                df.to_excel(
 650                    writer,
 651                    sheet_name=iType,
 652                    encoding="UTF-8",
 653                    freeze_panes=(1, 1),
 654                )  # saving as XLSX-file with freeze first row and column as headers
 655
 656        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 657
 658    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 659        """
 660        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 661        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 662
 663        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 664
 665        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 666                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 667        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 668        """
 669        if self.iListDumpFile is None or not self.iListDumpFile:
 670            uLogger.error("Output name of dump file must be defined!")
 671            raise Exception("Filename required")
 672
 673        if not self.iList or forceUpdate:
 674            self.iList = self.Listing()
 675
 676        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 677        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 678            fH.write(jsonDump)
 679
 680        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 681
 682        return jsonDump
 683
 684    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
 685        """
 686        Show information about one instrument defined by json data and prints it in Markdown format.
 687
 688        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 689
 690        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 691        :param show: if `True` then also printing information about instrument and its current price.
 692        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
 693        :return: multilines text in Markdown format with information about one instrument.
 694        """
 695        splitLine = "|                                                             |                                                        |\n"
 696        infoText = ""
 697
 698        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 699            info = [
 700                "# Main information\n\n",
 701                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 702                "| Parameters                                                  | Values                                                 |\n",
 703                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 704                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 705                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 706            ]
 707
 708            if "sector" in iJSON.keys() and iJSON["sector"]:
 709                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 710
 711            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 712                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 713
 714            info.extend([
 715                splitLine,
 716                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 717                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 718            ])
 719
 720            if "isin" in iJSON.keys() and iJSON["isin"]:
 721                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 722
 723            if "classCode" in iJSON.keys():
 724                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 725
 726            info.extend([
 727                splitLine,
 728                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 729                splitLine,
 730                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 731                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 732                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 733            ])
 734
 735            if iJSON["figi"]:
 736                self._figi = iJSON["figi"]
 737                iJSON = iJSON | self.RequestTradingStatus()
 738
 739                info.extend([
 740                    splitLine,
 741                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 742                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 743                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 744                ])
 745
 746            info.append(splitLine)
 747
 748            if "type" in iJSON.keys() and iJSON["type"]:
 749                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 750
 751                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 752                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 753
 754            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 755                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 756
 757            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 758                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 759
 760            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 761                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 762
 763            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 764                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 765
 766            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 767                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 768
 769            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 770                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 771
 772            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 773                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 774
 775            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 776                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 777
 778            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 779                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 780
 781            if "currency" in iJSON.keys():
 782                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 783
 784            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 785                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 786
 787            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 788                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 789
 790            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 791                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 792
 793            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 794                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 795
 796            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 797                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 798
 799            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 800                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 801
 802            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 803                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 804
 805            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 806                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 807
 808            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 809                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 810
 811            iExt = None
 812            if iJSON["type"] == "Bonds":
 813                info.extend([
 814                    splitLine,
 815                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 816                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 817                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 818                        iJSON["nominal"]["currency"],
 819                    )),
 820                ])
 821
 822                if "floatingCouponFlag" in iJSON.keys():
 823                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 824
 825                if "amortizationFlag" in iJSON.keys():
 826                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 827
 828                info.append(splitLine)
 829
 830                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 831                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 832
 833                if iJSON["figi"]:
 834                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 835
 836                    info.extend([
 837                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 838                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 839                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 840                    ])
 841
 842                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 843                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 844                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 845                        iJSON["aciValue"]["currency"]
 846                    )))
 847
 848            if "currentPrice" in iJSON.keys():
 849                info.append(splitLine)
 850
 851                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 852                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 853
 854                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 855                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 856                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 857                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 858                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 859
 860                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 861                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 862
 863                info.extend([
 864                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 865                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 866                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 867                    )),
 868                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 869                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 870                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 871                    )),
 872                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 873                        "{:.2f}%{}".format(
 874                            iJSON["currentPrice"]["changes"],
 875                            " ({}{:.2f} {})".format(
 876                                "+" if bondChangesDelta > 0 else "",
 877                                bondChangesDelta,
 878                                aciCurrency
 879                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 880                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 881                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 882                                currency
 883                            ),
 884                        )
 885                    ),
 886                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 887                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 888                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 889                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 890                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 891                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 892                    )),
 893                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 894                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 895                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 896                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 897                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 898                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 899                    )),
 900                ])
 901
 902            if "lot" in iJSON.keys():
 903                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 904
 905            if "step" in iJSON.keys() and iJSON["step"] != 0:
 906                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 907
 908            # Add bond payment calendar:
 909            if iJSON["type"] == "Bonds":
 910                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 911                info.extend(["\n#", strCalendar])
 912
 913            infoText += "".join(info)
 914
 915            if show and not onlyFiles:
 916                uLogger.info("{}".format(infoText))
 917
 918            if self.infoFile is not None and (show or onlyFiles):
 919                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 920                    fH.write(infoText)
 921
 922                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 923
 924                if self.useHTMLReports:
 925                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 926                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 927                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 928
 929                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 930
 931        return infoText
 932
 933    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 934        """
 935        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 936
 937        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 938        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 939        :return: JSON formatted data with information about instrument.
 940        """
 941        tickerJSON = {}
 942        if self.moreDebug:
 943            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 944
 945        if not self._ticker:
 946            uLogger.warning("self._ticker variable is not be empty!")
 947
 948        else:
 949            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 950                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 951                raise Exception("Instrument not allowed")
 952
 953            if not self.iList:
 954                self.iList = self.Listing()
 955
 956            if self._ticker in self.iList["Shares"].keys():
 957                tickerJSON = self.iList["Shares"][self._ticker]
 958                if self.moreDebug:
 959                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 960
 961            elif self._ticker in self.iList["Currencies"].keys():
 962                tickerJSON = self.iList["Currencies"][self._ticker]
 963                if self.moreDebug:
 964                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 965
 966            elif self._ticker in self.iList["Bonds"].keys():
 967                tickerJSON = self.iList["Bonds"][self._ticker]
 968                if self.moreDebug:
 969                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 970
 971            elif self._ticker in self.iList["Etfs"].keys():
 972                tickerJSON = self.iList["Etfs"][self._ticker]
 973                if self.moreDebug:
 974                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 975
 976            elif self._ticker in self.iList["Futures"].keys():
 977                tickerJSON = self.iList["Futures"][self._ticker]
 978                if self.moreDebug:
 979                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 980
 981        if tickerJSON:
 982            self._figi = tickerJSON["figi"]
 983
 984            if requestPrice:
 985                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 986
 987                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 988                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 989
 990                else:
 991                    tickerJSON["currentPrice"]["changes"] = 0
 992
 993            if show:
 994                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 995
 996        else:
 997            if show:
 998                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 999
1000        return tickerJSON
1001
1002    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1003        """
1004        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1005
1006        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1007        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1008        :return: JSON formatted data with information about instrument.
1009        """
1010        figiJSON = {}
1011        if self.moreDebug:
1012            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
1013
1014        if not self._figi:
1015            uLogger.warning("self._figi variable is not be empty!")
1016
1017        else:
1018            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1019                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
1020                raise Exception("Instrument not allowed")
1021
1022            if not self.iList:
1023                self.iList = self.Listing()
1024
1025            for item in self.iList["Shares"].keys():
1026                if self._figi == self.iList["Shares"][item]["figi"]:
1027                    figiJSON = self.iList["Shares"][item]
1028
1029                    if self.moreDebug:
1030                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1031
1032                    break
1033
1034            if not figiJSON:
1035                for item in self.iList["Currencies"].keys():
1036                    if self._figi == self.iList["Currencies"][item]["figi"]:
1037                        figiJSON = self.iList["Currencies"][item]
1038
1039                        if self.moreDebug:
1040                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1041
1042                        break
1043
1044            if not figiJSON:
1045                for item in self.iList["Bonds"].keys():
1046                    if self._figi == self.iList["Bonds"][item]["figi"]:
1047                        figiJSON = self.iList["Bonds"][item]
1048
1049                        if self.moreDebug:
1050                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1051
1052                        break
1053
1054            if not figiJSON:
1055                for item in self.iList["Etfs"].keys():
1056                    if self._figi == self.iList["Etfs"][item]["figi"]:
1057                        figiJSON = self.iList["Etfs"][item]
1058
1059                        if self.moreDebug:
1060                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1061
1062                        break
1063
1064            if not figiJSON:
1065                for item in self.iList["Futures"].keys():
1066                    if self._figi == self.iList["Futures"][item]["figi"]:
1067                        figiJSON = self.iList["Futures"][item]
1068
1069                        if self.moreDebug:
1070                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1071
1072                        break
1073
1074        if figiJSON:
1075            self._figi = figiJSON["figi"]
1076            self._ticker = figiJSON["ticker"]
1077
1078            if requestPrice:
1079                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1080
1081                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1082                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1083
1084                else:
1085                    figiJSON["currentPrice"]["changes"] = 0
1086
1087            if show:
1088                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1089
1090        else:
1091            if show:
1092                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1093
1094        return figiJSON
1095
1096    def GetCurrentPrices(self, show: bool = True) -> dict:
1097        """
1098        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1099        `{"buy": [{"price": 1243.8, "quantity": 193},
1100                  {"price": 1244.0, "quantity": 168},
1101                  {"price": 1244.8, "quantity": 5},
1102                  {"price": 1245.0, "quantity": 61},
1103                  {"price": 1245.4, "quantity": 60}],
1104          "sell": [{"price": 1243.6, "quantity": 8},
1105                   {"price": 1242.6, "quantity": 10},
1106                   {"price": 1242.4, "quantity": 18},
1107                   {"price": 1242.2, "quantity": 50},
1108                   {"price": 1242.0, "quantity": 113}],
1109          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1110        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1111        - sell: list of dicts with Buyers prices,
1112            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1113            - quantity: volume value by current price in lots,
1114        - limitUp: current trade session limit price, maximum,
1115        - limitDown: current trade session limit price, minimum,
1116        - lastPrice: last deal price of the instrument,
1117        - closePrice: previous trade session close price of the instrument.
1118
1119        See also: `SearchByTicker()` and `SearchByFIGI()`.
1120        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1121        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1122
1123        :param show: if `True` then print DOM to log and console.
1124        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1125                 If an error occurred then returns an empty record:
1126                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1127        """
1128        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1129
1130        if self.depth < 1:
1131            uLogger.error("Depth of Market (DOM) must be >=1!")
1132            raise Exception("Incorrect value")
1133
1134        if not (self._ticker or self._figi):
1135            uLogger.error("self._ticker or self._figi variables must be defined!")
1136            raise Exception("Ticker or FIGI required")
1137
1138        if self._ticker and not self._figi:
1139            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1140            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1141
1142        if not self._ticker and self._figi:
1143            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1144            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1145
1146        if not self._figi:
1147            uLogger.error("FIGI is not defined!")
1148            raise Exception("Ticker or FIGI required")
1149
1150        else:
1151            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1152
1153            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1154            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1155            self.body = str({"figi": self._figi, "depth": self.depth})
1156            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1157
1158            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1159                # list of dicts with sellers orders:
1160                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1161
1162                # list of dicts with buyers orders:
1163                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1164
1165                # max price of instrument at this time:
1166                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1167
1168                # min price of instrument at this time:
1169                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1170
1171                # last price of deal with instrument:
1172                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1173
1174                # last close price of instrument:
1175                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1176
1177            else:
1178                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1179                uLogger.debug("Server response: {}".format(pricesResponse))
1180
1181            if show:
1182                if prices["buy"] or prices["sell"]:
1183                    info = [
1184                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1185                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1186                            self._ticker,
1187                            self._figi,
1188                            self.depth,
1189                        ),
1190                        "-" * 60, "\n",
1191                        "             Orders of Buyers | Orders of Sellers\n",
1192                        "-" * 60, "\n",
1193                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1194                        "-" * 60, "\n",
1195                    ]
1196
1197                    if not prices["buy"]:
1198                        info.append("                              | No orders!\n")
1199                        sumBuy = 0
1200
1201                    else:
1202                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1203                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1204                        for item in maxMinSorted:
1205                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1206
1207                    if not prices["sell"]:
1208                        info.append("No orders!                    |\n")
1209                        sumSell = 0
1210
1211                    else:
1212                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1213                        for item in prices["sell"]:
1214                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1215
1216                    info.extend([
1217                        "-" * 60, "\n",
1218                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1219                        "-" * 60, "\n",
1220                    ])
1221
1222                    infoText = "".join(info)
1223
1224                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1225
1226                else:
1227                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1228
1229        return prices
1230
1231    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1232        """
1233        This method get and show information about all available broker instruments for current user account.
1234        If `instrumentsFile` string is not empty then also save information to this file.
1235
1236        :param show: if `True` then print results to console, if `False` — print only to file.
1237        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1238        :return: multi-lines string with all available broker instruments.
1239        """
1240        if not self.iList:
1241            self.iList = self.Listing()
1242
1243        info = [
1244            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1245            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1246        ]
1247
1248        # add instruments count by type:
1249        for iType in self.iList.keys():
1250            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1251
1252        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1253        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1254
1255        # generating info tables with all instruments by type:
1256        for iType in self.iList.keys():
1257            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1258
1259            for instrument in self.iList[iType].keys():
1260                iName = self.iList[iType][instrument]["name"]  # instrument's name
1261                if len(iName) > 57:
1262                    iName = "{}...".format(iName[:54])  # right trim for a long string
1263
1264                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1265                    self.iList[iType][instrument]["ticker"],
1266                    iName,
1267                    self.iList[iType][instrument]["figi"],
1268                    self.iList[iType][instrument]["currency"],
1269                    self.iList[iType][instrument]["lot"],
1270                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1271                ))
1272
1273        infoText = "".join(info)
1274
1275        if show and not onlyFiles:
1276            uLogger.info(infoText)
1277
1278        if self.instrumentsFile and (show or onlyFiles):
1279            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1280                fH.write(infoText)
1281
1282            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1283
1284            if self.useHTMLReports:
1285                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1286                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1287                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1288
1289                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1290
1291        return infoText
1292
1293    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1294        """
1295        This method search and show information about instruments by part of its ticker, FIGI or name.
1296        If `searchResultsFile` string is not empty then also save information to this file.
1297
1298        :param pattern: string with part of ticker, FIGI or instrument's name.
1299        :param show: if `True` then print results to console, if `False` — return list of result only.
1300        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1301        :return: list of dictionaries with all found instruments.
1302        """
1303        if not self.iList:
1304            self.iList = self.Listing()
1305
1306        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1307        compiledPattern = re.compile(pattern, re.IGNORECASE)
1308
1309        for iType in self.iList:
1310            for instrument in self.iList[iType].values():
1311                searchResult = compiledPattern.search(" ".join(
1312                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1313                ))
1314
1315                if searchResult:
1316                    searchResults[iType][instrument["ticker"]] = instrument
1317
1318        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1319        info = [
1320            "# Search results\n\n",
1321            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1322            "* **Search pattern:** [{}]\n".format(pattern),
1323            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1324            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1325        ]
1326        infoShort = info[:]
1327
1328        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1329        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1330        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1331
1332        if resultsLen == 0:
1333            info.append("\nNo results\n")
1334            infoShort.append("\nNo results\n")
1335            uLogger.warning("No results. Try changing your search pattern.")
1336
1337        else:
1338            for iType in searchResults:
1339                iTypeValuesCount = len(searchResults[iType].values())
1340                if iTypeValuesCount > 0:
1341                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1342                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1343
1344                    for instrument in searchResults[iType].values():
1345                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1346                            instrument["type"],
1347                            instrument["ticker"],
1348                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1349                            instrument["figi"],
1350                        ))
1351
1352                    if iTypeValuesCount <= 5:
1353                        infoShort.extend(info[-iTypeValuesCount:])
1354
1355                    else:
1356                        infoShort.extend(info[-5:])
1357                        infoShort.append(skippedLine)
1358
1359        infoText = "".join(info)
1360        infoTextShort = "".join(infoShort)
1361
1362        if show and not onlyFiles:
1363            uLogger.info(infoTextShort)
1364            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1365
1366        if self.searchResultsFile and (show or onlyFiles):
1367            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1368                fH.write(infoText)
1369
1370            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1371
1372            if self.useHTMLReports:
1373                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1374                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1375                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1376
1377                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1378
1379        return searchResults
1380
1381    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1382        """
1383        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1384
1385        :param instruments: list of strings with tickers or FIGIs.
1386        :return: list with unique instrument FIGIs only.
1387        """
1388        requestedInstruments = []
1389        for iName in instruments:
1390            if iName not in self.aliases.keys():
1391                if iName not in requestedInstruments:
1392                    requestedInstruments.append(iName)
1393
1394            else:
1395                if iName not in requestedInstruments:
1396                    if self.aliases[iName] not in requestedInstruments:
1397                        requestedInstruments.append(self.aliases[iName])
1398
1399        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1400
1401        onlyUniqueFIGIs = []
1402        for iName in requestedInstruments:
1403            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1404                continue
1405
1406            self._ticker = iName
1407            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1408
1409            if not iData:
1410                self._ticker = ""
1411                self._figi = iName
1412
1413                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1414
1415                if not iData:
1416                    self._figi = ""
1417                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1418
1419            if iData and iData["figi"] not in onlyUniqueFIGIs:
1420                onlyUniqueFIGIs.append(iData["figi"])
1421
1422        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1423
1424        return onlyUniqueFIGIs
1425
1426    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1427        """
1428        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1429
1430        See limits: https://tinkoff.github.io/investAPI/limits/
1431
1432        If `pricesFile` string is not empty then also save information to this file.
1433
1434        :param instruments: list of strings with tickers or FIGIs.
1435        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1436        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1437        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1438                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1439        """
1440        if instruments is None or not instruments:
1441            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1442            raise Exception("Ticker or FIGI required")
1443
1444        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1445
1446        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1447
1448        iList = []  # trying to get info and current prices about all unique instruments:
1449        for self._figi in onlyUniqueFIGIs:
1450            iData = self.SearchByFIGI(requestPrice=True, show=False)
1451            iList.append(iData)
1452
1453        self.ShowListOfPrices(iList, show, onlyFiles)
1454
1455        return iList
1456
1457    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1458        """
1459        Show table contains current prices of given instruments.
1460
1461        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1462                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1463        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1464        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1465        :return: multilines text in Markdown format as a table contains current prices.
1466        """
1467        infoText = ""
1468
1469        if show or self.pricesFile or onlyFiles:
1470            info = [
1471                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1472                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1473                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1474            ]
1475
1476            for item in iList:
1477                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1478                    item["ticker"],
1479                    item["figi"],
1480                    item["type"],
1481                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1482                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1483                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1484                    "{} / {}".format(
1485                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1486                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1487                    ),
1488                    "{} / {}".format(
1489                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1490                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1491                    ),
1492                    item["currency"],
1493                ))
1494
1495            infoText = "".join(info)
1496
1497            if show and not onlyFiles:
1498                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1499
1500            if self.pricesFile and (show or onlyFiles):
1501                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1502                    fH.write(infoText)
1503
1504                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1505
1506                if self.useHTMLReports:
1507                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1508                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1509                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1510
1511                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1512
1513        return infoText
1514
1515    def RequestTradingStatus(self) -> dict:
1516        """
1517        Requesting trading status for the instrument defined by `figi` variable.
1518
1519        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1520
1521        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1522
1523        :return: dictionary with trading status attributes. Response example:
1524                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1525                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1526        """
1527        if self._figi is None or not self._figi:
1528            uLogger.error("Variable `figi` must be defined for using this method!")
1529            raise Exception("FIGI required")
1530
1531        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1532
1533        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1534        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1535        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1536
1537        if self.moreDebug:
1538            uLogger.debug("Records about current trading status successfully received")
1539
1540        return tradingStatus
1541
1542    def RequestPortfolio(self) -> dict:
1543        """
1544        Requesting actual user's portfolio for current `accountId`.
1545
1546        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1547
1548        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1549
1550        :return: dictionary with user's portfolio.
1551        """
1552        if self.accountId is None or not self.accountId:
1553            uLogger.error("Variable `accountId` must be defined for using this method!")
1554            raise Exception("Account ID required")
1555
1556        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1557
1558        self.body = str({"accountId": self.accountId})
1559        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1560        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1561
1562        if self.moreDebug:
1563            uLogger.debug("Records about user's portfolio successfully received")
1564
1565        return rawPortfolio
1566
1567    def RequestPositions(self) -> dict:
1568        """
1569        Requesting open positions by currencies and instruments for current `accountId`.
1570
1571        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1572
1573        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1574
1575        :return: dictionary with open positions by instruments.
1576        """
1577        if self.accountId is None or not self.accountId:
1578            uLogger.error("Variable `accountId` must be defined for using this method!")
1579            raise Exception("Account ID required")
1580
1581        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1582
1583        self.body = str({"accountId": self.accountId})
1584        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1585        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1586
1587        if self.moreDebug:
1588            uLogger.debug("Records about current open positions successfully received")
1589
1590        return rawPositions
1591
1592    def RequestPendingOrders(self) -> list:
1593        """
1594        Requesting current actual pending limit orders for current `accountId`.
1595
1596        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1597
1598        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1599
1600        :return: list of dictionaries with pending limit orders.
1601        """
1602        if self.accountId is None or not self.accountId:
1603            uLogger.error("Variable `accountId` must be defined for using this method!")
1604            raise Exception("Account ID required")
1605
1606        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1607
1608        self.body = str({"accountId": self.accountId})
1609        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1610        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1611
1612        if "orders" in rawResponse.keys():
1613            rawOrders = rawResponse["orders"]
1614            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1615
1616        else:
1617            rawOrders = []
1618            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1619
1620        return rawOrders
1621
1622    def RequestStopOrders(self) -> list:
1623        """
1624        Requesting current actual stop orders for current `accountId`.
1625
1626        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1627
1628        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1629
1630        :return: list of dictionaries with stop orders.
1631        """
1632        if self.accountId is None or not self.accountId:
1633            uLogger.error("Variable `accountId` must be defined for using this method!")
1634            raise Exception("Account ID required")
1635
1636        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1637
1638        self.body = str({"accountId": self.accountId})
1639        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1640        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1641
1642        if "stopOrders" in rawResponse.keys():
1643            rawStopOrders = rawResponse["stopOrders"]
1644            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1645
1646        else:
1647            rawStopOrders = []
1648            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1649
1650        return rawStopOrders
1651
1652    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1653        """
1654        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1655        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1656        and `overviewBondsCalendarFile` are defined then also save information to file.
1657
1658        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1659        many requests about the state of the portfolio, and then, based on the received data, a large number
1660        of calculation and statistics are collected.
1661
1662        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1663        :param details: how detailed should the information be?
1664        - `full` — shows full available information about portfolio status (by default),
1665        - `positions` — shows only open positions,
1666        - `orders` — shows only sections of open limits and stop orders.
1667        - `digest` — show a short digest of the portfolio status,
1668        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1669        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1670        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1671        :return: dictionary with client's raw portfolio and some statistics.
1672        """
1673        if self.accountId is None or not self.accountId:
1674            uLogger.error("Variable `accountId` must be defined for using this method!")
1675            raise Exception("Account ID required")
1676
1677        view = {
1678            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1679                "headers": {},  # list of dictionaries, response headers without "positions" section
1680                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1681                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1682                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1683                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1684                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1685                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1686                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1687                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1688                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1689            },
1690            "stat": {  # --- some statistics calculated using "raw" sections:
1691                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1692                "availableRUB": 0.,  # available rubles (without other currencies)
1693                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1694                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1695                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1696                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1697                "sharesCostRUB": 0.,  # costs of all shares in RUB
1698                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1699                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1700                "futuresCostRUB": 0.,  # costs of all futures in RUB
1701                "Currencies": [],  # list of dictionaries of all currencies statistics
1702                "Shares": [],  # list of dictionaries of all shares statistics
1703                "Bonds": [],  # list of dictionaries of all bonds statistics
1704                "Etfs": [],  # list of dictionaries of all etfs statistics
1705                "Futures": [],  # list of dictionaries of all futures statistics
1706                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1707                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1708                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1709                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1710                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1711            },
1712            "analytics": {  # --- some analytics of portfolio:
1713                "distrByAssets": {},  # portfolio distribution by assets
1714                "distrByCompanies": {},  # portfolio distribution by companies
1715                "distrBySectors": {},  # portfolio distribution by sectors
1716                "distrByCurrencies": {},  # portfolio distribution by currencies
1717                "distrByCountries": {},  # portfolio distribution by countries
1718                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1719            }
1720        }
1721
1722        details = details.lower()
1723        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1724        if details not in availableDetails:
1725            details = "full"
1726            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1727
1728        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1729
1730        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1731        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1732        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1733        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1734
1735        # save response headers without "positions" section:
1736        for key in portfolioResponse.keys():
1737            if key != "positions":
1738                view["raw"]["headers"][key] = portfolioResponse[key]
1739
1740            else:
1741                continue
1742
1743        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1744        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1745        for item in portfolioResponse["positions"]:
1746            if item["instrumentType"] == "currency":
1747                self._figi = item["figi"]
1748                if not self._figi and item["ticker"]:
1749                    self._ticker = item["ticker"]
1750                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1751
1752                curr = self.SearchByFIGI(requestPrice=False)
1753
1754                # current price of currency in RUB:
1755                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1756                    "name": curr["name"],
1757                    "currentPrice": NanoToFloat(
1758                        item["currentPrice"]["units"],
1759                        item["currentPrice"]["nano"]
1760                    ),
1761                }
1762
1763                view["raw"]["Currencies"].append(item)
1764
1765            elif item["instrumentType"] == "share":
1766                view["raw"]["Shares"].append(item)
1767
1768            elif item["instrumentType"] == "bond":
1769                view["raw"]["Bonds"].append(item)
1770
1771            elif item["instrumentType"] == "etf":
1772                view["raw"]["Etfs"].append(item)
1773
1774            elif item["instrumentType"] == "futures":
1775                view["raw"]["Futures"].append(item)
1776
1777            else:
1778                continue
1779
1780        # how many volume of currencies (by ISO currency name) are blocked:
1781        for item in view["raw"]["positions"]["blocked"]:
1782            blocked = NanoToFloat(item["units"], item["nano"])
1783            if blocked > 0:
1784                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1785
1786        # how many volume of instruments (by FIGI) are blocked:
1787        for item in view["raw"]["positions"]["securities"]:
1788            blocked = int(item["blocked"])
1789            if blocked > 0:
1790                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1791
1792        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1793
1794        if "rub" in allBlocked.keys():
1795            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1796
1797        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1798        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1799        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1800        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1801        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1802        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1803        view["stat"]["portfolioCostRUB"] = sum([
1804            view["stat"]["allCurrenciesCostRUB"],
1805            view["stat"]["sharesCostRUB"],
1806            view["stat"]["bondsCostRUB"],
1807            view["stat"]["etfsCostRUB"],
1808            view["stat"]["futuresCostRUB"],
1809        ])
1810
1811        # --- calculating some portfolio statistics:
1812        byComp = {}  # distribution by companies
1813        bySect = {}  # distribution by sectors
1814        byCurr = {}  # distribution by currencies (include RUB)
1815        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1816        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1817
1818        for item in portfolioResponse["positions"]:
1819            self._figi = item["figi"]
1820            if not self._figi and item["ticker"]:
1821                self._ticker = item["ticker"]
1822                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1823
1824            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1825
1826            if instrument:
1827                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1828                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1829
1830                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1831                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1832
1833                else:
1834                    blocked = 0
1835
1836                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1837                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1838                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1839                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1840                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1841                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1842                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1843                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1844                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1845                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1846                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1847                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1848
1849                statData = {
1850                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1851                    "ticker": instrument["ticker"],  # ticker by FIGI
1852                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1853                    "volume": volume,  # available volume of instrument
1854                    "lots": lots,  # volume in lots of instrument
1855                    "direction": direction,  # direction of an instrument's position: short or long
1856                    "blocked": blocked,  # blocked volume of currency or instrument
1857                    "currentPrice": curPrice,  # current instrument's price in basic asset
1858                    "average": average,  # current average position price
1859                    "cost": cost,  # current cost of all volume of instrument in basic asset
1860                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1861                    "costRUB": costRUB,  # cost of instrument in ruble
1862                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1863                    "profit": profit,  # expected profit at current moment
1864                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1865                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1866                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1867                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1868                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1869                    "step": instrument["step"],  # minimum price increment
1870                }
1871
1872                # adding distribution by unique countries:
1873                if statData["country"] not in byCountry.keys():
1874                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1875
1876                else:
1877                    byCountry[statData["country"]]["cost"] += costRUB
1878                    byCountry[statData["country"]]["percent"] += percentCostRUB
1879
1880                if item["instrumentType"] != "currency":
1881                    # adding distribution by unique companies:
1882                    if statData["name"]:
1883                        if statData["name"] not in byComp.keys():
1884                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1885
1886                        else:
1887                            byComp[statData["name"]]["cost"] += costRUB
1888                            byComp[statData["name"]]["percent"] += percentCostRUB
1889
1890                    # adding distribution by unique sectors:
1891                    if statData["sector"] not in bySect.keys():
1892                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1893
1894                    else:
1895                        bySect[statData["sector"]]["cost"] += costRUB
1896                        bySect[statData["sector"]]["percent"] += percentCostRUB
1897
1898                # adding distribution by unique currencies:
1899                if currency not in byCurr.keys():
1900                    byCurr[currency] = {
1901                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1902                        "cost": costRUB,
1903                        "percent": percentCostRUB
1904                    }
1905
1906                else:
1907                    byCurr[currency]["cost"] += costRUB
1908                    byCurr[currency]["percent"] += percentCostRUB
1909
1910                # saving statistics for every instrument:
1911                if item["instrumentType"] == "currency":
1912                    view["stat"]["Currencies"].append(statData)
1913
1914                    # update dict with free funds for trading (total - blocked) by currencies
1915                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1916                    view["stat"]["funds"][currency] = {
1917                        "total": volume,
1918                        "totalCostRUB": costRUB,  # total volume cost in rubles
1919                        "free": volume - blocked,
1920                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1921                    }
1922
1923                elif item["instrumentType"] == "share":
1924                    view["stat"]["Shares"].append(statData)
1925
1926                elif item["instrumentType"] == "bond":
1927                    view["stat"]["Bonds"].append(statData)
1928
1929                elif item["instrumentType"] == "etf":
1930                    view["stat"]["Etfs"].append(statData)
1931
1932                elif item["instrumentType"] == "Futures":
1933                    view["stat"]["Futures"].append(statData)
1934
1935                else:
1936                    continue
1937
1938        # total changes in Russian Ruble:
1939        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1940        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1941        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1942        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1943        view["stat"]["funds"]["rub"] = {
1944            "total": view["stat"]["availableRUB"],
1945            "totalCostRUB": view["stat"]["availableRUB"],
1946            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1947            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1948        }
1949
1950        # --- pending limit orders sector data:
1951        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1952        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1953
1954        for item in view["raw"]["orders"]:
1955            self._figi = item["figi"]
1956
1957            if item["figi"] not in uniquePendingOrdersFIGIs:
1958                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1959
1960                uniquePendingOrdersFIGIs.append(item["figi"])
1961                uniquePendingOrders[item["figi"]] = instrument
1962
1963            else:
1964                instrument = uniquePendingOrders[item["figi"]]
1965
1966            if instrument:
1967                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1968                orderType = TKS_ORDER_TYPES[item["orderType"]]
1969                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1970                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1971
1972                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1973                if item["direction"] == "ORDER_DIRECTION_BUY":
1974                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1975
1976                else:
1977                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1978
1979                # requested price for order execution:
1980                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1981
1982                # necessary changes in percent to reach target from current price:
1983                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1984
1985                view["stat"]["orders"].append({
1986                    "orderID": item["orderId"],  # orderId number parameter of current order
1987                    "figi": item["figi"],  # FIGI identification
1988                    "ticker": instrument["ticker"],  # ticker name by FIGI
1989                    "lotsRequested": item["lotsRequested"],  # requested lots value
1990                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1991                    "currentPrice": lastPrice,  # current instrument's price for defined action
1992                    "targetPrice": target,  # requested price for order execution in base currency
1993                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1994                    "percentChanges": changes,  # changes in percent to target from current price
1995                    "currency": item["currency"],  # instrument's currency name
1996                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1997                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1998                    "status": orderState,  # order status from TKS_ORDER_STATES
1999                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
2000                })
2001
2002        # --- stop orders sector data:
2003        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
2004        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
2005
2006        for item in view["raw"]["stopOrders"]:
2007            self._figi = item["figi"]
2008
2009            if item["figi"] not in uniqueStopOrdersFIGIs:
2010                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
2011
2012                uniqueStopOrdersFIGIs.append(item["figi"])
2013                uniqueStopOrders[item["figi"]] = instrument
2014
2015            else:
2016                instrument = uniqueStopOrders[item["figi"]]
2017
2018            if instrument:
2019                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
2020                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
2021                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
2022
2023                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
2024                if "expirationTime" in item.keys():
2025                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
2026                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
2027
2028                else:
2029                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
2030                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2031
2032                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2033                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2034                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2035
2036                else:
2037                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2038
2039                # requested price when stop-order executed:
2040                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2041
2042                # price for limit-order, set up when stop-order executed:
2043                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2044
2045                # necessary changes in percent to reach target from current price:
2046                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2047
2048                view["stat"]["stopOrders"].append({
2049                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2050                    "figi": item["figi"],  # FIGI identification
2051                    "ticker": instrument["ticker"],  # ticker name by FIGI
2052                    "lotsRequested": item["lotsRequested"],  # requested lots value
2053                    "currentPrice": lastPrice,  # current instrument's price for defined action
2054                    "targetPrice": target,  # requested price for stop-order execution in base currency
2055                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2056                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2057                    "percentChanges": changes,  # changes in percent to target from current price
2058                    "currency": item["currency"],  # instrument's currency name
2059                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2060                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2061                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2062                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2063                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2064                })
2065
2066        # --- calculating data for analytics section:
2067        # portfolio distribution by assets:
2068        view["analytics"]["distrByAssets"] = {
2069            "Ruble": {
2070                "uniques": 1,
2071                "cost": view["stat"]["availableRUB"],
2072                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2073            },
2074            "Currencies": {
2075                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2076                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2077                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2078            },
2079            "Shares": {
2080                "uniques": len(view["stat"]["Shares"]),
2081                "cost": view["stat"]["sharesCostRUB"],
2082                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2083            },
2084            "Bonds": {
2085                "uniques": len(view["stat"]["Bonds"]),
2086                "cost": view["stat"]["bondsCostRUB"],
2087                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2088            },
2089            "Etfs": {
2090                "uniques": len(view["stat"]["Etfs"]),
2091                "cost": view["stat"]["etfsCostRUB"],
2092                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2093            },
2094            "Futures": {
2095                "uniques": len(view["stat"]["Futures"]),
2096                "cost": view["stat"]["futuresCostRUB"],
2097                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2098            },
2099        }
2100
2101        # portfolio distribution by companies:
2102        view["analytics"]["distrByCompanies"]["All money cash"] = {
2103            "ticker": "",
2104            "cost": view["stat"]["allCurrenciesCostRUB"],
2105            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2106        }
2107        view["analytics"]["distrByCompanies"].update(byComp)
2108
2109        # portfolio distribution by sectors:
2110        view["analytics"]["distrBySectors"]["All money cash"] = {
2111            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2112            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2113        }
2114        view["analytics"]["distrBySectors"].update(bySect)
2115
2116        # portfolio distribution by currencies:
2117        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2118            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2119
2120            if self.moreDebug:
2121                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2122
2123        view["analytics"]["distrByCurrencies"].update(byCurr)
2124        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2125        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2126
2127        # portfolio distribution by countries:
2128        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2129            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2130
2131            if self.moreDebug:
2132                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2133
2134        view["analytics"]["distrByCountries"].update(byCountry)
2135        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2136        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2137
2138        # --- Prepare text statistics overview in human-readable:
2139        if show or onlyFiles:
2140            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2141
2142            # Whatever the value `details`, header not changes:
2143            info = [
2144                "# Client's portfolio\n\n",
2145                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2146                "* **Account ID:** [{}]\n".format(self.accountId),
2147            ]
2148
2149            if details in ["full", "positions", "digest"]:
2150                info.extend([
2151                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2152                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2153                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2154                        view["stat"]["totalChangesRUB"],
2155                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2156                        view["stat"]["totalChangesPercentRUB"],
2157                    ),
2158                ])
2159
2160            if details in ["full", "positions"]:
2161                info.extend([
2162                    "## Open positions\n\n",
2163                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2164                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2165                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2166                        "{:.2f} ({:.2f}) rub".format(
2167                            view["stat"]["availableRUB"],
2168                            view["stat"]["blockedRUB"],
2169                        )
2170                    )
2171                ])
2172
2173                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2174                    return [
2175                        "|                             |                                 |          |              |              |                     |                              |\n",
2176                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2177                            noTradeStr if noTradeStr else typeStr,
2178                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2179                        ),
2180                    ]
2181
2182                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2183                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2184                        "{} [{}]".format(data["ticker"], data["figi"]),
2185                        "{:.2f} ({:.2f}) {}".format(
2186                            data["volume"],
2187                            data["blocked"],
2188                            data["currency"],
2189                        ) if isCurr else "{:.0f} ({:.0f})".format(
2190                            data["volume"],
2191                            data["blocked"],
2192                        ),
2193                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2194                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2195                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2196                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2197                        "{}{:.2f} {} ({}{:.2f}%)".format(
2198                            "+" if data["profit"] > 0 else "",
2199                            data["profit"], data["baseCurrencyName"],
2200                            "+" if data["percentProfit"] > 0 else "",
2201                            data["percentProfit"],
2202                        ),
2203                    )
2204
2205                # --- Show currencies section:
2206                if view["stat"]["Currencies"]:
2207                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2208                    for item in view["stat"]["Currencies"]:
2209                        info.append(_InfoStr(item, isCurr=True))
2210
2211                else:
2212                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2213
2214                # --- Show shares section:
2215                if view["stat"]["Shares"]:
2216                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2217
2218                    for item in view["stat"]["Shares"]:
2219                        info.append(_InfoStr(item))
2220
2221                else:
2222                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2223
2224                # --- Show bonds section:
2225                if view["stat"]["Bonds"]:
2226                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2227
2228                    for item in view["stat"]["Bonds"]:
2229                        info.append(_InfoStr(item))
2230
2231                else:
2232                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2233
2234                # --- Show etfs section:
2235                if view["stat"]["Etfs"]:
2236                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2237
2238                    for item in view["stat"]["Etfs"]:
2239                        info.append(_InfoStr(item))
2240
2241                else:
2242                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2243
2244                # --- Show futures section:
2245                if view["stat"]["Futures"]:
2246                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2247
2248                    for item in view["stat"]["Futures"]:
2249                        info.append(_InfoStr(item))
2250
2251                else:
2252                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2253
2254            if details in ["full", "orders"]:
2255                # --- Show pending limit orders section:
2256                if view["stat"]["orders"]:
2257                    info.extend([
2258                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2259                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2260                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2261                    ])
2262
2263                    for item in view["stat"]["orders"]:
2264                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2265                            "{} [{}]".format(item["ticker"], item["figi"]),
2266                            item["orderID"],
2267                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2268                            "{} {} ({}{:.2f}%)".format(
2269                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2270                                item["baseCurrencyName"],
2271                                "+" if item["percentChanges"] > 0 else "",
2272                                float(item["percentChanges"]),
2273                            ),
2274                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2275                            item["action"],
2276                            item["type"],
2277                            item["date"],
2278                        ))
2279
2280                else:
2281                    info.append("\n## Total pending limit-orders: [0]\n")
2282
2283                # --- Show stop orders section:
2284                if view["stat"]["stopOrders"]:
2285                    info.extend([
2286                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2287                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2288                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2289                    ])
2290
2291                    for item in view["stat"]["stopOrders"]:
2292                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2293                            "{} [{}]".format(item["ticker"], item["figi"]),
2294                            item["orderID"],
2295                            item["lotsRequested"],
2296                            "{} {} ({}{:.2f}%)".format(
2297                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2298                                item["baseCurrencyName"],
2299                                "+" if item["percentChanges"] > 0 else "",
2300                                float(item["percentChanges"]),
2301                            ),
2302                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2303                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2304                            item["action"],
2305                            item["type"],
2306                            item["expType"],
2307                            item["createDate"],
2308                            item["expDate"],
2309                        ))
2310
2311                else:
2312                    info.append("\n## Total stop-orders: [0]\n")
2313
2314            if details in ["full", "analytics"]:
2315                # -- Show analytics section:
2316                if view["stat"]["portfolioCostRUB"] > 0:
2317                    info.extend([
2318                        "\n# Analytics\n\n"
2319                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2320                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2321                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2322                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2323                            view["stat"]["totalChangesRUB"],
2324                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2325                            view["stat"]["totalChangesPercentRUB"],
2326                        ),
2327                        "\n## Portfolio distribution by assets\n"
2328                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2329                        "|------------------------------------|---------|---------|--------------------|\n",
2330                    ])
2331
2332                    for key in view["analytics"]["distrByAssets"].keys():
2333                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2334                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2335                                key,
2336                                view["analytics"]["distrByAssets"][key]["uniques"],
2337                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2338                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2339                            ))
2340
2341                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2342
2343                    info.extend([
2344                        "\n## Portfolio distribution by companies\n"
2345                        "\n| Company                                      | Percent | Current cost       |\n",
2346                        aSepLine,
2347                    ])
2348
2349                    for company in view["analytics"]["distrByCompanies"].keys():
2350                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2351                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2352                                "{}{}".format(
2353                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2354                                    company,
2355                                ),
2356                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2357                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2358                            ))
2359
2360                    info.extend([
2361                        "\n## Portfolio distribution by sectors\n"
2362                        "\n| Sector                                       | Percent | Current cost       |\n",
2363                        aSepLine,
2364                    ])
2365
2366                    for sector in view["analytics"]["distrBySectors"].keys():
2367                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2368                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2369                                sector,
2370                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2371                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2372                            ))
2373
2374                    info.extend([
2375                        "\n## Portfolio distribution by currencies\n"
2376                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2377                        aSepLine,
2378                    ])
2379
2380                    for curr in view["analytics"]["distrByCurrencies"].keys():
2381                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2382                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2383                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2384                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2385                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2386                            ))
2387
2388                    info.extend([
2389                        "\n## Portfolio distribution by countries\n"
2390                        "\n| Assets by country                            | Percent | Current cost       |\n",
2391                        aSepLine,
2392                    ])
2393
2394                    for country in view["analytics"]["distrByCountries"].keys():
2395                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2396                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2397                                country,
2398                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2399                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2400                            ))
2401
2402            if details in ["full", "calendar"]:
2403                # -- Show bonds payment calendar section:
2404                if view["stat"]["Bonds"]:
2405                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2406                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2407                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2408
2409                else:
2410                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2411
2412            infoText = "".join(info)
2413
2414            if show and not onlyFiles:
2415                uLogger.info(infoText)
2416
2417            if details == "full" and self.overviewFile:
2418                filename = self.overviewFile
2419
2420            elif details == "digest" and self.overviewDigestFile:
2421                filename = self.overviewDigestFile
2422
2423            elif details == "positions" and self.overviewPositionsFile:
2424                filename = self.overviewPositionsFile
2425
2426            elif details == "orders" and self.overviewOrdersFile:
2427                filename = self.overviewOrdersFile
2428
2429            elif details == "analytics" and self.overviewAnalyticsFile:
2430                filename = self.overviewAnalyticsFile
2431
2432            elif details == "calendar" and self.overviewBondsCalendarFile:
2433                filename = self.overviewBondsCalendarFile
2434
2435            else:
2436                filename = ""
2437
2438            if filename and (show or onlyFiles):
2439                with open(filename, "w", encoding="UTF-8") as fH:
2440                    fH.write(infoText)
2441
2442                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2443
2444                if self.useHTMLReports:
2445                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2446                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2447                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2448
2449                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2450
2451        return view
2452
2453    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2454        """
2455        Returns history operations between two given dates for current `accountId`.
2456        If `reportFile` string is not empty then also save human-readable report.
2457        Shows some statistical data of closed positions.
2458
2459        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2460        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2461        :param show: if `True` then also prints all records to the console.
2462        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2463        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2464        :return: original list of dictionaries with history of deals records from API ("operations" key):
2465                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2466                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2467        """
2468        if self.accountId is None or not self.accountId:
2469            uLogger.error("Variable `accountId` must be defined for using this method!")
2470            raise Exception("Account ID required")
2471
2472        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2473
2474        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2475
2476        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2477        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2478        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2479        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2480        customStat = {}  # custom statistics in additional to responseJSON
2481
2482        # --- output report in human-readable format:
2483        if self.reportFile and (show or onlyFiles):
2484            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2485            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2486            nextDay = ""
2487
2488            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2489
2490            if len(ops) > 0:
2491                customStat = {
2492                    "opsCount": 0,  # total operations count
2493                    "buyCount": 0,  # buy operations
2494                    "sellCount": 0,  # sell operations
2495                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2496                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2497                    "payIn": {"rub": 0.},  # Deposit brokerage account
2498                    "payOut": {"rub": 0.},  # Withdrawals
2499                    "divs": {"rub": 0.},  # Dividends income
2500                    "coupons": {"rub": 0.},  # Coupon's income
2501                    "brokerCom": {"rub": 0.},  # Service commissions
2502                    "serviceCom": {"rub": 0.},  # Service commissions
2503                    "marginCom": {"rub": 0.},  # Margin commissions
2504                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2505                }
2506
2507                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2508                for item in ops:
2509                    if item["state"] == "OPERATION_STATE_EXECUTED":
2510                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2511
2512                        # count buy operations:
2513                        if "_BUY" in item["operationType"]:
2514                            customStat["buyCount"] += 1
2515
2516                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2517                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2518
2519                            else:
2520                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2521
2522                        # count sell operations:
2523                        elif "_SELL" in item["operationType"]:
2524                            customStat["sellCount"] += 1
2525
2526                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2527                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2528
2529                            else:
2530                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2531
2532                        # count incoming operations:
2533                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2534                            if item["payment"]["currency"] in customStat["payIn"].keys():
2535                                customStat["payIn"][item["payment"]["currency"]] += payment
2536
2537                            else:
2538                                customStat["payIn"][item["payment"]["currency"]] = payment
2539
2540                        # count withdrawals operations:
2541                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2542                            if item["payment"]["currency"] in customStat["payOut"].keys():
2543                                customStat["payOut"][item["payment"]["currency"]] += payment
2544
2545                            else:
2546                                customStat["payOut"][item["payment"]["currency"]] = payment
2547
2548                        # count dividends income:
2549                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2550                            if item["payment"]["currency"] in customStat["divs"].keys():
2551                                customStat["divs"][item["payment"]["currency"]] += payment
2552
2553                            else:
2554                                customStat["divs"][item["payment"]["currency"]] = payment
2555
2556                        # count coupon's income:
2557                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2558                            if item["payment"]["currency"] in customStat["coupons"].keys():
2559                                customStat["coupons"][item["payment"]["currency"]] += payment
2560
2561                            else:
2562                                customStat["coupons"][item["payment"]["currency"]] = payment
2563
2564                        # count broker commissions:
2565                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2566                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2567                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2568
2569                            else:
2570                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2571
2572                        # count service commissions:
2573                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2574                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2575                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2576
2577                            else:
2578                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2579
2580                        # count margin commissions:
2581                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2582                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2583                                customStat["marginCom"][item["payment"]["currency"]] += payment
2584
2585                            else:
2586                                customStat["marginCom"][item["payment"]["currency"]] = payment
2587
2588                        # count withholding taxes:
2589                        elif "_TAX" in item["operationType"]:
2590                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2591                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2592
2593                            else:
2594                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2595
2596                        else:
2597                            continue
2598
2599                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2600
2601                # --- view "Actions" lines:
2602                info.extend([
2603                    "| Report sections            |                               |                              |                      |                        |\n",
2604                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2605                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2606                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2607                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2608                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2609                    ),
2610                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2611                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2612                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2613                    ),
2614                ])
2615
2616                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2617                for key in opsKeys:
2618                    if key == "rub":
2619                        continue
2620
2621                    info.extend([
2622                        "|                            |                               | {:<28} |                      |                        |\n".format(
2623                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2624                        ),
2625                        "|                            |                               | {:<28} |                      |                        |\n".format(
2626                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2627                        ),
2628                    ])
2629
2630                info.append(splitLine1)
2631
2632                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2633                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2634                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2635                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2636                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2637                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2638                    )
2639
2640                # --- view "Payments" lines:
2641                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2642                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2643
2644                for key in paymentsKeys:
2645                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2646
2647                info.append(splitLine1)
2648
2649                # --- view "Commissions and taxes" lines:
2650                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2651                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2652
2653                for key in comKeys:
2654                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2655
2656                info.extend([
2657                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2658                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2659                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2660                ])
2661
2662            else:
2663                info.append("Broker returned no operations during this period\n")
2664
2665            # --- view "Operations" section:
2666            for item in ops:
2667                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2668                    continue
2669
2670                else:
2671                    self._figi = item["figi"]
2672                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2673                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2674
2675                    # group of deals during one day:
2676                    if nextDay and item["date"].split("T")[0] != nextDay:
2677                        info.append(splitLine2)
2678                        nextDay = ""
2679
2680                    else:
2681                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2682
2683                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2684                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2685                        self._figi if self._figi else "—",
2686                        instrument["ticker"] if instrument else "—",
2687                        instrument["type"] if instrument else "—",
2688                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2689                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2690                        TKS_OPERATION_STATES[item["state"]],
2691                        TKS_OPERATION_TYPES[item["operationType"]],
2692                    ))
2693
2694            infoText = "".join(info)
2695
2696            if show and not onlyFiles:
2697                if self.moreDebug:
2698                    uLogger.debug("Records about history of a client's operations successfully received")
2699
2700                uLogger.info(infoText)
2701
2702            if self.reportFile and (show or onlyFiles):
2703                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2704                    fH.write(infoText)
2705
2706                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2707
2708                if self.useHTMLReports:
2709                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2710                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2711                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2712
2713                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2714
2715        return ops, customStat
2716
2717    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2718        """
2719        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2720
2721        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2722        Warning! Broker server used ISO UTC time by default.
2723
2724        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2725        Also, `historyFile` used to update history with `onlyMissing` parameter.
2726
2727        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2728
2729        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2730        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2731        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2732                         `"hour"`, `"day"`. Default: `"hour"`.
2733        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2734                            False by default. Warning! History appends only from last candle to current time
2735                            with always update last candle!
2736        :param csvSep: separator if csv-file is used, `,` by default.
2737        :param show: if `True` then also prints Pandas DataFrame to the console.
2738        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2739        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2740                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2741        """
2742        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2743        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2744        history = None  # empty pandas object for history
2745
2746        if interval not in TKS_CANDLE_INTERVALS.keys():
2747            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2748            raise Exception("Incorrect value")
2749
2750        if not (self._ticker or self._figi):
2751            uLogger.error("Ticker or FIGI must be defined!")
2752            raise Exception("Ticker or FIGI required")
2753
2754        if self._ticker and not self._figi:
2755            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2756            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2757
2758        if self._figi and not self._ticker:
2759            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2760            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2761
2762        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2763        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2764        if interval.lower() != "day":
2765            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2766
2767        delta = dtEnd - dtStart  # current UTC time minus last time in file
2768        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2769
2770        # calculate history length in candles:
2771        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2772        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2773            length += 1  # to avoid fraction time
2774
2775        # calculate data blocks count:
2776        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2777
2778        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2779        if self.moreDebug:
2780            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2781            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2782            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2783            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2784
2785        tempOld = None  # pandas object for old history, if --only-missing key present
2786        lastTime = None  # datetime object of last old candle in file
2787
2788        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2789            if self.moreDebug:
2790                uLogger.debug("--only-missing key present, add only last missing candles...")
2791                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2792
2793            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2794
2795            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2796            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2797            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2798            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2799
2800            # get last datetime object from last string in file or minus 1 delta if file is empty:
2801            if len(tempOld) > 0:
2802                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2803
2804            else:
2805                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2806
2807            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2808
2809        responseJSONs = []  # raw history blocks of data
2810
2811        blockEnd = dtEnd
2812        for item in range(blocks):
2813            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2814            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2815
2816            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2817                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2818            ))
2819
2820            if blockStart == blockEnd:
2821                uLogger.debug("Skipped this zero-length block...")
2822
2823            else:
2824                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2825                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2826                self.body = str({
2827                    "figi": self._figi,
2828                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2829                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2830                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2831                })
2832                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2833
2834                if "code" in responseJSON.keys():
2835                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2836
2837                else:
2838                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2839                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2840
2841                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2842
2843            blockEnd = blockStart
2844
2845        printCount = len(responseJSONs)  # candles to show in console
2846        if responseJSONs:
2847            tempHistory = pd.DataFrame(
2848                data={
2849                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2850                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2851                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2852                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2853                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2854                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2855                    "volume": [int(item["volume"]) for item in responseJSONs],
2856                },
2857                index=range(len(responseJSONs)),
2858                columns=["date", "time", "open", "high", "low", "close", "volume"],
2859            )
2860            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2861            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2862
2863            # append only newest candles to old history if --only-missing key present:
2864            if onlyMissing and tempOld is not None and lastTime is not None:
2865                index = 0  # find start index in tempHistory data:
2866
2867                for i, item in tempHistory.iterrows():
2868                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2869
2870                    if curTime == lastTime:
2871                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2872                        index = i
2873                        printCount = index + 1
2874                        break
2875
2876                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2877
2878            else:
2879                history = tempHistory  # if no `--only-missing` key then load full data from server
2880
2881            if self.moreDebug:
2882                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2883
2884        if history is not None and not history.empty:
2885            if show and not onlyFiles:
2886                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2887                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2888                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2889                ))
2890
2891        else:
2892            uLogger.warning("Received an empty candles history!")
2893
2894        if self.historyFile is not None:
2895            if history is not None and not history.empty:
2896                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2897                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2898
2899            else:
2900                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2901
2902        else:
2903            if self.moreDebug:
2904                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2905
2906        return history
2907
2908    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2909        """
2910        Load candles history from csv-file and return Pandas DataFrame object.
2911
2912        See also: `History()` and `ShowHistoryChart()` methods.
2913
2914        :param filePath: path to csv-file to open.
2915        """
2916        loadedHistory = None  # init candles data object
2917
2918        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2919
2920        if os.path.exists(filePath):
2921            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2922
2923            tfStr = self.priceModel.FormattedDelta(
2924                self.priceModel.timeframe,
2925                "{days} days {hours}h {minutes}m {seconds}s",
2926            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2927                self.priceModel.timeframe,
2928                "{hours}h {minutes}m {seconds}s",
2929            )
2930
2931            if loadedHistory is not None and not loadedHistory.empty:
2932                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2933                    len(loadedHistory),
2934                    tfStr,
2935                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2936                )
2937
2938            else:
2939                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2940
2941        else:
2942            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2943
2944        return loadedHistory
2945
2946    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2947        """
2948        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2949
2950        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2951        Default: `index.html` (both for interact and non-interact candlesticks chart).
2952
2953        See also: `History()` and `LoadHistory()` methods.
2954
2955        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2956        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2957                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2958                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2959                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2960        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2961                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2962        """
2963        if isinstance(candles, str):
2964            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2965            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2966
2967        elif isinstance(candles, pd.DataFrame):
2968            self.priceModel.prices = candles  # set candles chain from variable
2969            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2970
2971            if "datetime" not in candles.columns:
2972                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2973
2974        else:
2975            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2976            raise Exception("Incorrect value")
2977
2978        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2979
2980        if interact:
2981            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2982
2983            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2984
2985        else:
2986            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2987
2988            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2989
2990        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2991
2992    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2993        """
2994        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2995        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2996
2997        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2998
2999        :param operation: string "Buy" or "Sell".
3000        :param lots: volume, integer count of lots >= 1.
3001        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
3002        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
3003        :param expDate: string "Undefined" by default or local date in future,
3004                        it is a string with format `%Y-%m-%d %H:%M:%S`.
3005        :return: JSON with response from broker server.
3006        """
3007        if self.accountId is None or not self.accountId:
3008            uLogger.error("Variable `accountId` must be defined for using this method!")
3009            raise Exception("Account ID required")
3010
3011        if operation is None or not operation or operation not in ("Buy", "Sell"):
3012            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3013            raise Exception("Incorrect value")
3014
3015        if lots is None or lots < 1:
3016            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
3017            lots = 1
3018
3019        if tp is None or tp < 0:
3020            tp = 0
3021
3022        if sl is None or sl < 0:
3023            sl = 0
3024
3025        if expDate is None or not expDate:
3026            expDate = "Undefined"
3027
3028        if not (self._ticker or self._figi):
3029            uLogger.error("Ticker or FIGI must be defined!")
3030            raise Exception("Ticker or FIGI required")
3031
3032        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3033        self._ticker = instrument["ticker"]
3034        self._figi = instrument["figi"]
3035
3036        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3037
3038        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3039        self.body = str({
3040            "figi": self._figi,
3041            "quantity": str(lots),
3042            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3043            "accountId": str(self.accountId),
3044            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3045        })
3046        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3047
3048        if "orderId" in response.keys():
3049            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3050                operation, response["orderId"],
3051                self._ticker, self._figi, lots,
3052                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3053                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3054                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3055            ))
3056
3057            if tp > 0:
3058                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3059
3060            if sl > 0:
3061                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3062
3063        else:
3064            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3065
3066        return response
3067
3068    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3069        """
3070        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3071        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3072
3073        See also: `Order()` and `Trade()` docstrings.
3074
3075        :param lots: volume, integer count of lots >= 1.
3076        :param tp: float > 0, take profit price of stop-order.
3077        :param sl: float > 0, stop loss price of stop-order.
3078        :param expDate: it's a local date in future.
3079                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3080        :return: JSON with response from broker server.
3081        """
3082        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3083
3084    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3085        """
3086        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3087        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3088
3089        See also: `Order()` and `Trade()` docstrings.
3090
3091        :param lots: volume, integer count of lots >= 1.
3092        :param tp: float > 0, take profit price of stop-order.
3093        :param sl: float > 0, stop loss price of stop-order.
3094        :param expDate: it's a local date in the future.
3095                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3096        :return: JSON with response from broker server.
3097        """
3098        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3099
3100    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3101        """
3102        Close position of given instruments.
3103
3104        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3105        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3106                         This avoids unnecessary downloading data from the server.
3107        """
3108        if instruments is None or not instruments:
3109            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3110            raise Exception("Ticker or FIGI required")
3111
3112        if isinstance(instruments, str):
3113            instruments = [instruments]
3114
3115        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3116        if uniqueInstruments:
3117            if portfolio is None or not portfolio:
3118                portfolio = self.Overview(show=False)
3119
3120            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3121            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3122
3123            for self._figi in uniqueInstruments:
3124                if self._figi not in allOpened:
3125                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3126                    continue
3127
3128                # search open trade info about instrument by ticker:
3129                instrument = {}
3130                for iType in TKS_INSTRUMENTS:
3131                    if instrument:
3132                        break
3133
3134                    for item in portfolio["stat"][iType]:
3135                        if item["figi"] == self._figi:
3136                            instrument = item
3137                            break
3138
3139                if instrument:
3140                    self._ticker = instrument["ticker"]
3141                    self._figi = instrument["figi"]
3142
3143                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3144                        self._ticker,
3145                        self._figi,
3146                        int(instrument["volume"]),
3147                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3148                    ))
3149
3150                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3151
3152                    if tradeLots > 0:
3153                        if instrument["blocked"] > 0:
3154                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3155                                instrument["blocked"],
3156                                self._ticker,
3157                                tradeLots,
3158                            ))
3159
3160                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3161                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3162
3163                    else:
3164                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3165
3166    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3167        """
3168        Close all positions of given instruments with defined type.
3169
3170        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3171        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3172                         This avoids unnecessary downloading data from the server.
3173        """
3174        if iType not in TKS_INSTRUMENTS:
3175            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3176
3177        else:
3178            if portfolio is None or not portfolio:
3179                portfolio = self.Overview(show=False)
3180
3181            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3182            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3183
3184            if tickers and portfolio:
3185                self.CloseTrades(tickers, portfolio)
3186
3187            else:
3188                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3189
3190    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3191        """
3192        Universal method to create market or limit orders with all available parameters for current `accountId`.
3193        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3194
3195        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3196        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3197
3198        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3199        then broker immediately open market order as you can do simple --buy or --sell operations!
3200
3201        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3202        When current price will go up or down to target price value then broker opens a limit order.
3203        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3204
3205        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3206
3207        :param operation: string "Buy" or "Sell".
3208        :param orderType: string "Limit" or "Stop".
3209        :param lots: volume, integer count of lots >= 1.
3210        :param targetPrice: target price > 0. This is open trade price for limit order.
3211        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3212                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3213        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3214                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3215                         Stop loss order always executed by market price.
3216        :param expDate: string "Undefined" by default or local date in future.
3217                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3218                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3219                        A limit order has no expiration date, it lasts until the end of the trading day.
3220        :return: JSON with response from broker server.
3221        """
3222        if self.accountId is None or not self.accountId:
3223            uLogger.error("Variable `accountId` must be defined for using this method!")
3224            raise Exception("Account ID required")
3225
3226        if operation is None or not operation or operation not in ("Buy", "Sell"):
3227            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3228            raise Exception("Incorrect value")
3229
3230        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3231            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3232            raise Exception("Incorrect value")
3233
3234        if lots is None or lots < 1:
3235            uLogger.error("You must define trade volume > 0: integer count of lots!")
3236            raise Exception("Incorrect value")
3237
3238        if targetPrice is None or targetPrice <= 0:
3239            uLogger.error("Target price for limit-order must be greater than 0!")
3240            raise Exception("Incorrect value")
3241
3242        if limitPrice is None or limitPrice <= 0:
3243            limitPrice = targetPrice
3244
3245        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3246            stopType = "Limit"
3247
3248        if expDate is None or not expDate:
3249            expDate = "Undefined"
3250
3251        if not (self._ticker or self._figi):
3252            uLogger.error("Tocker or FIGI must be defined!")
3253            raise Exception("Ticker or FIGI required")
3254
3255        response = {}
3256        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3257        self._ticker = instrument["ticker"]
3258        self._figi = instrument["figi"]
3259
3260        if orderType == "Limit":
3261            uLogger.debug(
3262                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3263                    self._ticker, self._figi,
3264                    operation, lots, targetPrice, instrument["currency"],
3265                ))
3266
3267            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3268            self.body = str({
3269                "figi": self._figi,
3270                "quantity": str(lots),
3271                "price": FloatToNano(targetPrice),
3272                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3273                "accountId": str(self.accountId),
3274                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3275            })
3276            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3277
3278            if "orderId" in response.keys():
3279                uLogger.info(
3280                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3281                        response["orderId"], self._ticker, self._figi, operation, lots,
3282                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3283                    ))
3284
3285                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3286                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3287                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3288                            targetPrice, instrument["currency"],
3289                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3290                        ))
3291
3292                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3293                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3294                            targetPrice, instrument["currency"],
3295                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3296                        ))
3297
3298            else:
3299                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3300
3301        if orderType == "Stop":
3302            uLogger.debug(
3303                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3304                    self._ticker, self._figi,
3305                    operation, lots,
3306                    targetPrice, instrument["currency"],
3307                    limitPrice, instrument["currency"],
3308                    stopType, expDate,
3309                ))
3310
3311            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3312            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3313            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3314
3315            body = {
3316                "figi": self._figi,
3317                "quantity": str(lots),
3318                "price": FloatToNano(limitPrice),
3319                "stopPrice": FloatToNano(targetPrice),
3320                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3321                "accountId": str(self.accountId),
3322                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3323                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3324            }
3325
3326            if expDateUTC:
3327                body["expireDate"] = expDateUTC
3328
3329            self.body = str(body)
3330            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3331
3332            if "stopOrderId" in response.keys():
3333                uLogger.info(
3334                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3335                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3336                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3337                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3338                        TKS_STOP_ORDER_TYPES[stopOrderType],
3339                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3340                    ))
3341
3342                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3343                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3344                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3345                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3346                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3347                        ))
3348
3349                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3350                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3351                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3352                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3353                        ))
3354
3355            else:
3356                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3357
3358        return response
3359
3360    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3361        """
3362        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3363        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3364        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3365        See also: `Order()` docstring.
3366
3367        :param lots: volume, integer count of lots >= 1.
3368        :param targetPrice: target price > 0. This is open trade price for limit order.
3369        :return: JSON with response from broker server.
3370        """
3371        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3372
3373    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3374        """
3375        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3376        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3377        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3378        target price value then broker opens a limit order. See also: `Order()` docstring.
3379
3380        :param lots: volume, integer count of lots >= 1.
3381        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3382        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3383                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3384        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3385                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3386        :param expDate: string "Undefined" by default or local date in future.
3387                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3388                        This date is converting to UTC format for server.
3389        :return: JSON with response from broker server.
3390        """
3391        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3392
3393    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3394        """
3395        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3396        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3397        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3398        See also: `Order()` docstring.
3399
3400        :param lots: volume, integer count of lots >= 1.
3401        :param targetPrice: target price > 0. This is open trade price for limit order.
3402        :return: JSON with response from broker server.
3403        """
3404        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3405
3406    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3407        """
3408        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3409        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3410        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3411        target price value then broker opens a limit order. See also: `Order()` docstring.
3412
3413        :param lots: volume, integer count of lots >= 1.
3414        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3415        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3416                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3417        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3418                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3419        :param expDate: string "Undefined" by default or local date in future.
3420                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3421                        This date is converting to UTC format for server.
3422        :return: JSON with response from broker server.
3423        """
3424        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3425
3426    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3427        """
3428        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3429
3430        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3431        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3432                             This avoids unnecessary downloading data from the server.
3433        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3434        """
3435        if self.accountId is None or not self.accountId:
3436            uLogger.error("Variable `accountId` must be defined for using this method!")
3437            raise Exception("Account ID required")
3438
3439        if orderIDs:
3440            if allOrdersIDs is None:
3441                rawOrders = self.RequestPendingOrders()
3442                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3443
3444            if allStopOrdersIDs is None:
3445                rawStopOrders = self.RequestStopOrders()
3446                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3447
3448            for orderID in orderIDs:
3449                idInPendingOrders = orderID in allOrdersIDs
3450                idInStopOrders = orderID in allStopOrdersIDs
3451
3452                if not (idInPendingOrders or idInStopOrders):
3453                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3454                    continue
3455
3456                else:
3457                    if idInPendingOrders:
3458                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3459
3460                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3461                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3462                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3463                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3464
3465                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3466                            if self.moreDebug:
3467                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3468
3469                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3470
3471                        else:
3472                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3473
3474                    elif idInStopOrders:
3475                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3476
3477                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3478                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3479                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3480                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3481
3482                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3483                            if self.moreDebug:
3484                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3485
3486                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3487
3488                        else:
3489                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3490
3491                    else:
3492                        continue
3493
3494    def CloseAllOrders(self) -> None:
3495        """
3496        Gets a list of open pending and stop orders and cancel it all.
3497        """
3498        rawOrders = self.RequestPendingOrders()
3499        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3500        lenOrders = len(allOrdersIDs)
3501
3502        rawStopOrders = self.RequestStopOrders()
3503        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3504        lenSOrders = len(allStopOrdersIDs)
3505
3506        if lenOrders > 0 or lenSOrders > 0:
3507            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3508
3509            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3510
3511        else:
3512            uLogger.info("Orders not found, nothing to cancel.")
3513
3514    def CloseAll(self, *args) -> None:
3515        """
3516        Close all available (not blocked) opened trades and orders.
3517
3518        Also, you can select one or more keywords case-insensitive:
3519        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3520
3521        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3522        """
3523        overview = self.Overview(show=False)  # get all open trades info
3524
3525        if len(args) == 0:
3526            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3527            self.CloseAllOrders()  # close all pending and stop orders
3528
3529            for iType in TKS_INSTRUMENTS:
3530                if iType != "Currencies":
3531                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3532
3533        else:
3534            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3535            lowerArgs = [x.lower() for x in args]
3536
3537            if "orders" in lowerArgs:
3538                self.CloseAllOrders()  # close all pending and stop orders
3539
3540            for iType in TKS_INSTRUMENTS:
3541                if iType.lower() in lowerArgs and iType != "Currencies":
3542                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3543
3544    def CloseAllByTicker(self, instrument: str) -> None:
3545        """
3546        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3547
3548        This method searches opened trade and orders of instrument throw all portfolio and then use
3549        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3550
3551        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3552
3553        :param instrument: string with ticker.
3554        """
3555        if instrument is None or not instrument:
3556            uLogger.error("Ticker name must be defined for using this method!")
3557            raise Exception("Ticker required")
3558
3559        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3560
3561        self._ticker = instrument  # try to set instrument as ticker
3562        self._figi = ""
3563
3564        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3565        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3566
3567        if limitAll and self.IsInLimitOrders(portfolio=overview):
3568            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3569            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3570
3571        if stopAll and self.IsInStopOrders(portfolio=overview):
3572            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3573            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3574
3575        if self.IsInPortfolio(portfolio=overview):
3576            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3577            self.CloseTrades(instruments=[instrument], portfolio=overview)
3578
3579    def CloseAllByFIGI(self, instrument: str) -> None:
3580        """
3581        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3582
3583        This method searches opened trade and orders of instrument throw all portfolio and then use
3584        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3585
3586        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3587
3588        :param instrument: string with FIGI id.
3589        """
3590        if instrument is None or not instrument:
3591            uLogger.error("FIGI id must be defined for using this method!")
3592            raise Exception("FIGI required")
3593
3594        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3595
3596        self._ticker = ""
3597        self._figi = instrument  # try to set instrument as FIGI id
3598
3599        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3600        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3601
3602        if limitAll and self.IsInLimitOrders(portfolio=overview):
3603            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3604            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3605
3606        if stopAll and self.IsInStopOrders(portfolio=overview):
3607            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3608            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3609
3610        if self.IsInPortfolio(portfolio=overview):
3611            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3612            self.CloseTrades(instruments=[instrument], portfolio=overview)
3613
3614    @staticmethod
3615    def ParseOrderParameters(operation, **inputParameters):
3616        """
3617        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3618
3619        :param operation: string "Buy" or "Sell".
3620        :param inputParameters: this is dict of strings that looks like this
3621               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3622               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3623               "prices" key: one or more prices to open limit-orders
3624               Counts of values in lots and prices lists must be equals!
3625        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3626        """
3627        # TODO: update order grid work with api v2
3628        pass
3629        # uLogger.debug("Input parameters: {}".format(inputParameters))
3630        #
3631        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3632        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3633        #     raise Exception("Incorrect value")
3634        #
3635        # if "l" in inputParameters.keys():
3636        #     inputParameters["lots"] = inputParameters.pop("l")
3637        #
3638        # if "p" in inputParameters.keys():
3639        #     inputParameters["prices"] = inputParameters.pop("p")
3640        #
3641        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3642        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3643        #     raise Exception("Incorrect value")
3644        #
3645        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3646        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3647        #
3648        # if len(lots) != len(prices):
3649        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3650        #     raise Exception("Incorrect value")
3651        #
3652        # uLogger.debug("Extracted parameters for orders:")
3653        # uLogger.debug("lots = {}".format(lots))
3654        # uLogger.debug("prices = {}".format(prices))
3655        #
3656        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3657        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3658        # uLogger.debug("Order parameters: {}".format(result))
3659        #
3660        # return result
3661
3662    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3663        """
3664        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3665
3666        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3667        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3668        """
3669        result = False
3670        msg = "Instrument not defined!"
3671
3672        if portfolio is None or not portfolio:
3673            portfolio = self.Overview(show=False)
3674
3675        if self._ticker:
3676            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3677            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3678
3679            for iType in TKS_INSTRUMENTS:
3680                for instrument in portfolio["stat"][iType]:
3681                    if instrument["ticker"] == self._ticker:
3682                        result = True
3683                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3684                        break
3685
3686        elif self._figi:
3687            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3688            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3689
3690            for iType in TKS_INSTRUMENTS:
3691                for instrument in portfolio["stat"][iType]:
3692                    if instrument["figi"] == self._figi:
3693                        result = True
3694                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3695                        break
3696
3697        else:
3698            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3699
3700        uLogger.debug(msg)
3701
3702        return result
3703
3704    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3705        """
3706        Returns instrument from the user's portfolio if it presents there.
3707        Instrument must be defined by `ticker` (highly priority) or `figi`.
3708
3709        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3710        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3711        """
3712        result = None
3713        msg = "Instrument not defined!"
3714
3715        if portfolio is None or not portfolio:
3716            portfolio = self.Overview(show=False)
3717
3718        if self._ticker:
3719            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3720            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3721
3722            for iType in TKS_INSTRUMENTS:
3723                for instrument in portfolio["stat"][iType]:
3724                    if instrument["ticker"] == self._ticker:
3725                        result = instrument
3726                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3727                        break
3728
3729        elif self._figi:
3730            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3731            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3732
3733            for iType in TKS_INSTRUMENTS:
3734                for instrument in portfolio["stat"][iType]:
3735                    if instrument["figi"] == self._figi:
3736                        result = instrument
3737                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3738                        break
3739
3740        else:
3741            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3742
3743        uLogger.debug(msg)
3744
3745        return result
3746
3747    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3748        """
3749        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3750
3751        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3752
3753        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3754        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3755        """
3756        result = False
3757        msg = "Instrument not defined!"
3758
3759        if portfolio is None or not portfolio:
3760            portfolio = self.Overview(show=False)
3761
3762        if self._ticker:
3763            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3764            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3765
3766            for instrument in portfolio["stat"]["orders"]:
3767                if instrument["ticker"] == self._ticker:
3768                    result = True
3769                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3770                    break
3771
3772        elif self._figi:
3773            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3774            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3775
3776            for instrument in portfolio["stat"]["orders"]:
3777                if instrument["figi"] == self._figi:
3778                    result = True
3779                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3780                    break
3781
3782        else:
3783            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3784
3785        uLogger.debug(msg)
3786
3787        return result
3788
3789    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3790        """
3791        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3792        Instrument must be defined by `ticker` (highly priority) or `figi`.
3793
3794        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3795
3796        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3797        :return: list with `orderID`s of limit orders.
3798        """
3799        result = []
3800        msg = "Instrument not defined!"
3801
3802        if portfolio is None or not portfolio:
3803            portfolio = self.Overview(show=False)
3804
3805        if self._ticker:
3806            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3807            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3808
3809            for instrument in portfolio["stat"]["orders"]:
3810                if instrument["ticker"] == self._ticker:
3811                    result.append(instrument["orderID"])
3812
3813            if result:
3814                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3815
3816        elif self._figi:
3817            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3818            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3819
3820            for instrument in portfolio["stat"]["orders"]:
3821                if instrument["figi"] == self._figi:
3822                    result.append(instrument["orderID"])
3823
3824            if result:
3825                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3826
3827        else:
3828            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3829
3830        uLogger.debug(msg)
3831
3832        return result
3833
3834    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3835        """
3836        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3837
3838        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3839
3840        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3841        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3842        """
3843        result = False
3844        msg = "Instrument not defined!"
3845
3846        if portfolio is None or not portfolio:
3847            portfolio = self.Overview(show=False)
3848
3849        if self._ticker:
3850            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3851            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3852
3853            for instrument in portfolio["stat"]["stopOrders"]:
3854                if instrument["ticker"] == self._ticker:
3855                    result = True
3856                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3857                    break
3858
3859        elif self._figi:
3860            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3861            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3862
3863            for instrument in portfolio["stat"]["stopOrders"]:
3864                if instrument["figi"] == self._figi:
3865                    result = True
3866                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3867                    break
3868
3869        else:
3870            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3871
3872        uLogger.debug(msg)
3873
3874        return result
3875
3876    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3877        """
3878        Returns list with all `orderID`s of opened stop orders for the instrument.
3879        Instrument must be defined by `ticker` (highly priority) or `figi`.
3880
3881        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3882
3883        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3884        :return: list with `orderID`s of stop orders.
3885        """
3886        result = []
3887        msg = "Instrument not defined!"
3888
3889        if portfolio is None or not portfolio:
3890            portfolio = self.Overview(show=False)
3891
3892        if self._ticker:
3893            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3894            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3895
3896            for instrument in portfolio["stat"]["stopOrders"]:
3897                if instrument["ticker"] == self._ticker:
3898                    result.append(instrument["orderID"])
3899
3900            if result:
3901                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3902
3903        elif self._figi:
3904            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3905            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3906
3907            for instrument in portfolio["stat"]["stopOrders"]:
3908                if instrument["figi"] == self._figi:
3909                    result.append(instrument["orderID"])
3910
3911            if result:
3912                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3913
3914        else:
3915            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3916
3917        uLogger.debug(msg)
3918
3919        return result
3920
3921    def RequestLimits(self) -> dict:
3922        """
3923        Method for obtaining the available funds for withdrawal for current `accountId`.
3924
3925        See also:
3926        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3927        - `OverviewLimits()` method
3928
3929        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3930                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3931                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3932                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3933        """
3934        if self.accountId is None or not self.accountId:
3935            uLogger.error("Variable `accountId` must be defined for using this method!")
3936            raise Exception("Account ID required")
3937
3938        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3939
3940        self.body = str({"accountId": self.accountId})
3941        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3942        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3943
3944        if self.moreDebug:
3945            uLogger.debug("Records about available funds for withdrawal successfully received")
3946
3947        return rawLimits
3948
3949    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3950        """
3951        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3952
3953        See also: `RequestLimits()`.
3954
3955        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3956        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3957        :return: dict with raw parsed data from server and some calculated statistics about it.
3958        """
3959        if self.accountId is None or not self.accountId:
3960            uLogger.error("Variable `accountId` must be defined for using this method!")
3961            raise Exception("Account ID required")
3962
3963        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3964
3965        view = {
3966            "rawLimits": rawLimits,
3967            "limits": {  # parsed data for every currency:
3968                "money": {  # this is an array of portfolio currency positions
3969                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3970                },
3971                "blocked": {  # this is an array of blocked currency
3972                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3973                },
3974                "blockedGuarantee": {  # this is locked money under collateral for futures
3975                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3976                },
3977            },
3978        }
3979
3980        # --- Prepare text table with limits in human-readable format:
3981        if show or onlyFiles:
3982            info = [
3983                "# Withdrawal limits\n\n",
3984                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3985                "* **Account ID:** [{}]\n".format(self.accountId),
3986            ]
3987
3988            if view["limits"]["money"]:
3989                info.extend([
3990                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3991                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3992                ])
3993
3994            else:
3995                info.append("\nNo withdrawal limits\n")
3996
3997            for curr in view["limits"]["money"].keys():
3998                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3999                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
4000                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
4001
4002                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
4003                    "[{}]".format(curr),
4004                    "{:.2f}".format(view["limits"]["money"][curr]),
4005                    "{:.2f}".format(availableMoney),
4006                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
4007                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
4008                )
4009
4010                if curr == "rub":
4011                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
4012
4013                else:
4014                    info.append(infoStr)
4015
4016            infoText = "".join(info)
4017
4018            if show and not onlyFiles:
4019                uLogger.info(infoText)
4020
4021            if self.withdrawalLimitsFile and (show or onlyFiles):
4022                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
4023                    fH.write(infoText)
4024
4025                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
4026
4027                if self.useHTMLReports:
4028                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
4029                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4030                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4031
4032                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4033
4034        return view
4035
4036    def RequestAccounts(self) -> dict:
4037        """
4038        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4039
4040        See also:
4041        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4042        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4043        - `OverviewUserInfo()` method
4044
4045        :return: dict with raw data from server that contains accounts info. Example of dict:
4046                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4047                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4048                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4049                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4050        """
4051        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4052
4053        self.body = str({})
4054        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4055        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4056
4057        if self.moreDebug:
4058            uLogger.debug("Records about available accounts successfully received")
4059
4060        return rawAccounts
4061
4062    def RequestUserInfo(self) -> dict:
4063        """
4064        Method for requesting common user's information.
4065
4066        See also:
4067        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4068        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4069        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4070        - `OverviewUserInfo()` method
4071
4072        :return: dict with raw data from server that contains user's information. Example of dict:
4073                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4074                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4075        """
4076        uLogger.debug("Requesting common user's information. Wait, please...")
4077
4078        self.body = str({})
4079        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4080        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4081
4082        if self.moreDebug:
4083            uLogger.debug("Records about current user successfully received")
4084
4085        return rawUserInfo
4086
4087    def RequestMarginStatus(self, accountId: str = None) -> dict:
4088        """
4089        Method for requesting margin calculation for defined account ID.
4090
4091        See also:
4092        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4093        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4094        - `OverviewUserInfo()` method
4095
4096        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4097        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4098                 Example of responses:
4099                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4100                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4101                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4102                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4103                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4104                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4105        """
4106        if accountId is None or not accountId:
4107            if self.accountId is None or not self.accountId:
4108                uLogger.error("Variable `accountId` must be defined for using this method!")
4109                raise Exception("Account ID required")
4110
4111            else:
4112                accountId = self.accountId  # use `self.accountId` (main ID) by default
4113
4114        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4115
4116        self.body = str({"accountId": accountId})
4117        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4118        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4119
4120        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4121            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4122            rawMargin = {}
4123
4124        else:
4125            if self.moreDebug:
4126                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4127
4128        return rawMargin
4129
4130    def RequestTariffLimits(self) -> dict:
4131        """
4132        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4133
4134        See also:
4135        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4136        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4137        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4138        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4139        - `OverviewUserInfo()` method
4140
4141        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4142                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4143                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4144        """
4145        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4146
4147        self.body = str({})
4148        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4149        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4150
4151        if self.moreDebug:
4152            uLogger.debug("Records with limits of current tariff successfully received")
4153
4154        return rawTariffLimits
4155
4156    def RequestBondCoupons(self, iJSON: dict) -> dict:
4157        """
4158        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4159        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4160        All dates are in UTC timezone.
4161
4162        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4163        Documentation:
4164        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4165        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4166
4167        See also: `ExtendBondsData()`.
4168
4169        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4170                      If raw iJSON is not data of bond then server returns an error [400] with message:
4171                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4172        :return: dictionary with bond payment calendar. Response example
4173                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4174                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4175                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4176                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4177        """
4178        if iJSON["figi"] is None or not iJSON["figi"]:
4179            uLogger.error("FIGI must be defined for using this method!")
4180            raise Exception("FIGI required")
4181
4182        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4183        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4184
4185        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4186            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4187            self._figi,
4188            startDate,
4189            endDate,
4190        ))
4191
4192        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4193        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4194        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4195
4196        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4197            uLogger.warning("Instrument type is not bond!")
4198
4199        else:
4200            if self.moreDebug:
4201                uLogger.debug("Records about bond payment calendar successfully received")
4202
4203        return calendar
4204
4205    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4206        """
4207        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4208        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4209        coupon yields, current yields and some statistics etc.
4210
4211        WARNING! This is too long operation if a lot of bonds requested from broker server.
4212
4213        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4214
4215        :param instruments: list of strings with tickers or FIGIs.
4216        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4217                     for further used by data scientists or stock analytics.
4218        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4219                 In XLSX-file and Pandas DataFrame fields mean:
4220                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4221                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4222        """
4223        if instruments is None or not instruments:
4224            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4225            raise Exception("Ticker or FIGI required")
4226
4227        if isinstance(instruments, str):
4228            instruments = [instruments]
4229
4230        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4231
4232        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4233
4234        iCount = len(uniqueInstruments)
4235        tooLong = iCount >= 20
4236        if tooLong:
4237            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4238
4239        bonds = None
4240        for i, self._figi in enumerate(uniqueInstruments):
4241            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4242
4243            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4244                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4245                rawBond = self.SearchByFIGI(requestPrice=True)
4246
4247                # Widen raw data with UTC current time (iData["actualDateTime"]):
4248                actualDate = datetime.now(tzutc())
4249                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4250
4251                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4252                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4253
4254                # Replace some values with human-readable:
4255                iData["nominalCurrency"] = iData["nominal"]["currency"]
4256                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4257                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4258                iData["aciCurrency"] = iData["aciValue"]["currency"]
4259                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4260                iData["issueSize"] = int(iData["issueSize"])
4261                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4262                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4263                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4264                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4265                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4266                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4267                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4268                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4269                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4270                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4271
4272                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4273                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4274                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4275                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4276                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4277                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4278                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4279                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4280                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4281                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4282                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4283
4284                # Widen raw data with calendar data from `rawCalendar` values:
4285                calendarData = []
4286                if "events" in iData["rawCalendar"].keys():
4287                    for item in iData["rawCalendar"]["events"]:
4288                        calendarData.append({
4289                            "couponDate": item["couponDate"],
4290                            "couponNumber": int(item["couponNumber"]),
4291                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4292                            "payCurrency": item["payOneBond"]["currency"],
4293                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4294                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4295                            "couponStartDate": item["couponStartDate"],
4296                            "couponEndDate": item["couponEndDate"],
4297                            "couponPeriod": item["couponPeriod"],
4298                        })
4299
4300                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4301                    if "maturityDate" not in iData.keys():
4302                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4303
4304                # Widen raw data with Coupon Rate.
4305                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4306                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4307                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4308                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4309
4310                # Widen raw data with Yield to Maturity (YTM) on current date.
4311                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4312                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4313                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4314                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4315                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4316                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4317
4318                iData["calendar"] = calendarData  # adds calendar at the end
4319
4320                # Remove not used data:
4321                iData.pop("uid")
4322                iData.pop("positionUid")
4323                iData.pop("currentPrice")
4324                iData.pop("rawCalendar")
4325
4326                colNames = list(iData.keys())
4327                if bonds is None:
4328                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4329
4330                else:
4331                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4332
4333            else:
4334                uLogger.warning("Instrument is not a bond!")
4335
4336            processed = round(100 * (i + 1) / iCount, 1)
4337            if tooLong and processed % 5 == 0:
4338                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4339
4340            else:
4341                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4342
4343        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4344
4345        # Saving bonds from Pandas DataFrame to XLSX sheet:
4346        if xlsx and self.bondsXLSXFile:
4347            with pd.ExcelWriter(
4348                    path=self.bondsXLSXFile,
4349                    date_format=TKS_DATE_FORMAT,
4350                    datetime_format=TKS_DATE_TIME_FORMAT,
4351                    mode="w",
4352            ) as writer:
4353                bonds.to_excel(
4354                    writer,
4355                    sheet_name="Extended bonds data",
4356                    index=True,
4357                    encoding="UTF-8",
4358                    freeze_panes=(1, 1),
4359                )  # saving as XLSX-file with freeze first row and column as headers
4360
4361            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4362
4363        return bonds
4364
4365    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4366        """
4367        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4368
4369        WARNING! This is too long operation if a lot of bonds requested from broker server.
4370
4371        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4372
4373        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4374                        extended information about bonds: main info, current prices, bond payment calendar,
4375                        coupon yields, current yields and some statistics etc.
4376                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4377        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4378                     for further used by data scientists or stock analytics.
4379        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4380        """
4381        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4382            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4383
4384        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4385
4386        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4387        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4388        calendar = None
4389        for bond in extBonds.iterrows():
4390            for item in bond[1]["calendar"]:
4391                cData = {
4392                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4393                    "couponDate": item["couponDate"],
4394                    "figi": bond[1]["figi"],
4395                    "ticker": bond[1]["ticker"],
4396                    "name": bond[1]["name"],
4397                    "couponNumber": item["couponNumber"],
4398                    "payOneBond": item["payOneBond"],
4399                    "payCurrency": item["payCurrency"],
4400                    "couponType": item["couponType"],
4401                    "couponPeriod": item["couponPeriod"],
4402                    "fixDate": item["fixDate"],
4403                    "couponStartDate": item["couponStartDate"],
4404                    "couponEndDate": item["couponEndDate"],
4405                }
4406
4407                if calendar is None:
4408                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4409
4410                else:
4411                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4412
4413        if calendar is not None:
4414            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4415
4416            # Saving calendar from Pandas DataFrame to XLSX sheet:
4417            if xlsx:
4418                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4419
4420                with pd.ExcelWriter(
4421                        path=xlsxCalendarFile,
4422                        date_format=TKS_DATE_FORMAT,
4423                        datetime_format=TKS_DATE_TIME_FORMAT,
4424                        mode="w",
4425                ) as writer:
4426                    humanReadable = calendar.copy(deep=True)
4427                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4428                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4429                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4430                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4431                    humanReadable.columns = colNames  # human-readable column names
4432
4433                    humanReadable.to_excel(
4434                        writer,
4435                        sheet_name="Bond payments calendar",
4436                        index=False,
4437                        encoding="UTF-8",
4438                        freeze_panes=(1, 2),
4439                    )  # saving as XLSX-file with freeze first row and column as headers
4440
4441                    del humanReadable  # release df in memory
4442
4443                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4444
4445        return calendar
4446
4447    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4448        """
4449        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4450        Also, creates Markdown file with calendar data, `calendar.md` by default.
4451
4452        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4453
4454        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4455                        extended information about bonds: main info, current prices, bond payment calendar,
4456                        coupon yields, current yields and some statistics etc.
4457                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4458        :param show: if `True` then also printing bonds payment calendar to the console,
4459                     otherwise save to file `calendarFile` only. `False` by default.
4460        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4461        :return: multilines text in Markdown format with bonds payment calendar as a table.
4462        """
4463        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4464            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4465
4466        infoText = "# Bond payments calendar\n\n"
4467
4468        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4469
4470        if not (calendar is None or calendar.empty):
4471            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4472
4473            info = [
4474                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4475                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4476                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4477            ]
4478
4479            newMonth = False
4480            notOneBond = calendar["figi"].nunique() > 1
4481            for i, bond in enumerate(calendar.iterrows()):
4482                if newMonth and notOneBond:
4483                    info.append(splitLine)
4484
4485                info.append(
4486                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4487                        "  √" if bond[1]["paid"] else "  —",
4488                        bond[1]["couponDate"].split("T")[0],
4489                        bond[1]["figi"],
4490                        bond[1]["ticker"],
4491                        bond[1]["couponNumber"],
4492                        "{} {}".format(
4493                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4494                            bond[1]["payCurrency"],
4495                        ),
4496                        bond[1]["couponType"],
4497                        bond[1]["couponPeriod"],
4498                        bond[1]["fixDate"].split("T")[0],
4499                    )
4500                )
4501
4502                if i < len(calendar.values) - 1:
4503                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4504                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4505                    newMonth = False if curDate.month == nextDate.month else True
4506
4507                else:
4508                    newMonth = False
4509
4510            infoText += "".join(info)
4511
4512            if show and not onlyFiles:
4513                uLogger.info("{}".format(infoText))
4514
4515            if self.calendarFile is not None and (show or onlyFiles):
4516                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4517                    fH.write(infoText)
4518
4519                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4520
4521                if self.useHTMLReports:
4522                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4523                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4524                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4525
4526                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4527
4528        else:
4529            infoText += "No data\n"
4530
4531        return infoText
4532
4533    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4534        """
4535        Method for parsing and show simple table with all available user accounts.
4536
4537        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4538
4539        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4540        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4541        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4542                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4543                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4544                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4545                                                        "closed": "—", "access": "Full access" }, ...}}`
4546        """
4547        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4548
4549        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4550        accounts = {
4551            item["id"]: {
4552                "type": TKS_ACCOUNT_TYPES[item["type"]],
4553                "name": item["name"],
4554                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4555                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4556                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4557                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4558            } for item in rawAccounts["accounts"]
4559        }
4560
4561        # Raw and parsed data with some fields replaced in "stat" section:
4562        view = {
4563            "rawAccounts": rawAccounts,
4564            "stat": accounts,
4565        }
4566
4567        # --- Prepare simple text table with only accounts data in human-readable format:
4568        if show or onlyFiles:
4569            info = [
4570                "# User accounts\n\n",
4571                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4572                "| Account ID   | Type                      | Status                    | Name                           |\n",
4573                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4574            ]
4575
4576            for account in view["stat"].keys():
4577                info.extend([
4578                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4579                        account,
4580                        view["stat"][account]["type"],
4581                        view["stat"][account]["status"],
4582                        view["stat"][account]["name"],
4583                    )
4584                ])
4585
4586            infoText = "".join(info)
4587
4588            if show and not onlyFiles:
4589                uLogger.info(infoText)
4590
4591            if self.userAccountsFile and (show or onlyFiles):
4592                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4593                    fH.write(infoText)
4594
4595                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4596
4597                if self.useHTMLReports:
4598                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4599                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4600                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4601
4602                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4603
4604        return view
4605
4606    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4607        """
4608        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4609
4610        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4611
4612        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4613        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4614        :return: dict with raw parsed data from server and some calculated statistics about it.
4615        """
4616        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4617        tmpTicker = self._ticker
4618        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4619        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4620        self._ticker = tmpTicker
4621
4622        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4623        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4624        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4625        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4626        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4627        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4628
4629        # This is dict with parsed common user data:
4630        userInfo = {
4631            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4632            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4633            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4634            "tariff": rawUserInfo["tariff"],
4635        }
4636
4637        # This is an array of dict with parsed margin statuses for every account IDs:
4638        margins = {}
4639        for accountId in accounts.keys():
4640            if rawMargins[accountId]:
4641                margins[accountId] = {
4642                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4643                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4644                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4645                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4646                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4647                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4648                    "missing": missing["volume"],
4649                }
4650
4651            else:
4652                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4653
4654        unary = {}  # unary-connection limits
4655        for item in rawTariffLimits["unaryLimits"]:
4656            if item["limitPerMinute"] in unary.keys():
4657                unary[item["limitPerMinute"]].extend(item["methods"])
4658
4659            else:
4660                unary[item["limitPerMinute"]] = item["methods"]
4661
4662        stream = {}  # stream-connection limits
4663        for item in rawTariffLimits["streamLimits"]:
4664            if item["limit"] in stream.keys():
4665                stream[item["limit"]].extend(item["streams"])
4666
4667            else:
4668                stream[item["limit"]] = item["streams"]
4669
4670        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4671        limits = {
4672            "unary": unary,
4673            "stream": stream,
4674        }
4675
4676        # Raw and parsed data as an output result:
4677        view = {
4678            "rawUserInfo": rawUserInfo,
4679            "rawAccounts": rawAccounts,
4680            "rawMargins": rawMargins,
4681            "rawTariffLimits": rawTariffLimits,
4682            "stat": {
4683                "overview": overview,
4684                "userInfo": userInfo,
4685                "accounts": accounts,
4686                "margins": margins,
4687                "limits": limits,
4688            },
4689        }
4690
4691        # --- Prepare text table with user information in human-readable format:
4692        if show or onlyFiles:
4693            info = [
4694                "# Full user information\n\n",
4695                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4696                "## Common information\n\n",
4697                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4698                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4699                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4700                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4701                "\n## User accounts\n\n",
4702            ]
4703
4704            for account in view["stat"]["accounts"].keys():
4705                info.extend([
4706                    "### ID: [{}]\n\n".format(account),
4707                    "| Parameters           | Values                                                       |\n",
4708                    "|----------------------|--------------------------------------------------------------|\n",
4709                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4710                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4711                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4712                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4713                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4714                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4715                ])
4716
4717                if margins[account]:
4718                    info.extend([
4719                        "| Margin status:       | Enabled                                                      |\n",
4720                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4721                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4722                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4723                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4724                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4725                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4726                    ])
4727
4728                else:
4729                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4730
4731            info.extend([
4732                "\n## Current user tariff limits\n",
4733                "\n### See also\n",
4734                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4735                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4736                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4737                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4738                "\n### Unary limits\n",
4739            ])
4740
4741            if unary:
4742                for key, values in sorted(unary.items()):
4743                    info.append("\n* Max requests per minute: {}\n".format(key))
4744
4745                    for value in values:
4746                        info.append("  - {}\n".format(value))
4747
4748            else:
4749                info.append("\nNot available\n")
4750
4751            info.append("\n### Stream limits\n")
4752
4753            if stream:
4754                for key, values in sorted(stream.items()):
4755                    info.append("\n* Max stream connections: {}\n".format(key))
4756
4757                    for value in values:
4758                        info.append("  - {}\n".format(value))
4759
4760            else:
4761                info.append("\nNot available\n")
4762
4763            infoText = "".join(info)
4764
4765            if show and not onlyFiles:
4766                uLogger.info(infoText)
4767
4768            if self.userInfoFile and (show or onlyFiles):
4769                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4770                    fH.write(infoText)
4771
4772                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4773
4774                if self.useHTMLReports:
4775                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4776                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4777                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4778
4779                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4780
4781        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 88    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 89        """
 90        Main class init.
 91
 92        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 93        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 94                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 95        :param useCache: use default cache file with raw data to use instead of `iList`.
 96                         True by default. Cache is auto-update if new day has come.
 97                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 98        :param defaultCache: path to default cache file. `dump.json` by default.
 99        """
100        if token is None or not token:
101            try:
102                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
103                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
104
105            except KeyError:
106                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
107                raise Exception("Token required")
108
109        else:
110            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
111            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
112
113        if accountId is None or not accountId:
114            try:
115                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
116                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
117
118            except KeyError:
119                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
120
121        else:
122            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
123            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
124
125        self.version = __version__  # duplicate here used TKSBrokerAPI main version
126        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
127
128        Latest version: https://pypi.org/project/tksbrokerapi/
129        """
130
131        self._tag = ""
132        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
133
134        self.__lock = Lock()  # initialize multiprocessing mutex lock
135
136        self._precision = 4  # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file
137
138        self.aliases = TKS_TICKER_ALIASES
139        """Some aliases instead official tickers.
140
141        See also: `TKSEnums.TKS_TICKER_ALIASES`
142        """
143
144        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
145
146        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
147
148        self._ticker = ""
149        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
150
151        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
152        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
153
154        See also: `SearchByTicker()`, `SearchInstruments()`.
155        """
156
157        self._figi = ""
158        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
159
160        See also: `SearchByFIGI()`, `SearchInstruments()`.
161        """
162
163        self.depth = 1
164        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
165
166        See also: `GetCurrentPrices()`.
167        """
168
169        self.server = r"https://invest-public-api.tinkoff.ru/rest"
170        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
171
172        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
173        """
174
175        uLogger.debug("Broker API server: {}".format(self.server))
176
177        self.timeout = 15
178        """Server operations timeout in seconds. Default: `15`.
179
180        See also: `SendAPIRequest()`.
181        """
182
183        self.headers = {
184            "Content-Type": "application/json",
185            "accept": "application/json",
186            "Authorization": "Bearer {}".format(self.token),
187            "x-app-name": "Tim55667757.TKSBrokerAPI",
188        }
189        """
190        Headers which send in every request to broker server. Please, do not change it!
191        Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`.
192
193        See also: `SendAPIRequest()`.
194        """
195
196        self.body = None
197        """Request body which send to broker server. Default: `None`.
198
199        See also: `SendAPIRequest()`.
200        """
201
202        self.moreDebug = False
203        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
204
205        self.useHTMLReports = False
206        """
207        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
208        
209        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
210        """
211
212        self.historyFile = None
213        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
214
215        See also: `History()`.
216        """
217
218        self.htmlHistoryFile = "index.html"
219        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
220
221        See also: `ShowHistoryChart()`.
222        """
223
224        self.instrumentsFile = "instruments.md"
225        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
226
227        See also: `ShowInstrumentsInfo()`.
228        """
229
230        self.searchResultsFile = "search-results.md"
231        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
232
233        See also: `SearchInstruments()`.
234        """
235
236        self.pricesFile = "prices.md"
237        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
238
239        See also: `GetListOfPrices()`.
240        """
241
242        self.infoFile = "info.md"
243        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
244
245        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
246        """
247
248        self.bondsXLSXFile = "ext-bonds.xlsx"
249        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
250        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
251
252        See also: `ExtendBondsData()`.
253        """
254
255        self.calendarFile = "calendar.md"
256        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
257        
258        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
259
260        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
261        """
262
263        self.overviewFile = "overview.md"
264        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
265
266        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
267        """
268
269        self.overviewDigestFile = "overview-digest.md"
270        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
271
272        See also: `Overview()` with parameter `details="digest"`.
273        """
274
275        self.overviewPositionsFile = "overview-positions.md"
276        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
277
278        See also: `Overview()` with parameter `details="positions"`.
279        """
280
281        self.overviewOrdersFile = "overview-orders.md"
282        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
283
284        See also: `Overview()` with parameter `details="orders"`.
285        """
286
287        self.overviewAnalyticsFile = "overview-analytics.md"
288        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
289
290        See also: `Overview()` with parameter `details="analytics"`.
291        """
292
293        self.overviewBondsCalendarFile = "overview-calendar.md"
294        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
295
296        See also: `Overview()` with parameter `details="calendar"`.
297        """
298
299        self.reportFile = "deals.md"
300        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
301
302        See also: `Deals()`.
303        """
304
305        self.withdrawalLimitsFile = "limits.md"
306        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
307
308        See also: `OverviewLimits()` and `RequestLimits()`.
309        """
310
311        self.userInfoFile = "user-info.md"
312        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
313
314        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
315        """
316
317        self.userAccountsFile = "accounts.md"
318        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
319
320        See also: `OverviewAccounts()`, `RequestAccounts()`.
321        """
322
323        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
324        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
325
326        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
327
328        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
329        """
330
331        self.iList = None  # init iList for raw instruments data
332        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
333        
334        See also: `Listing()`, `DumpInstruments()`.
335        """
336
337        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
338        if useCache:
339            if os.path.exists(self.iListDumpFile):
340                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
341                curTime = datetime.now(tzutc())
342
343                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
344                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
345
346                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
347
348                else:
349                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
350
351                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
352                        os.path.abspath(self.iListDumpFile),
353                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
354                    ))
355
356            else:
357                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
358                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
359
360        else:
361            self.iList = self.Listing()  # request new raw instruments data from broker server
362            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
363
364        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
365        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
366
367        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
368        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

useHTMLReports

If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.

See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

tag: str

Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: "" (empty string).

ticker: str

Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi: str

Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
450    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
451        """
452        Send GET or POST request to broker server and receive JSON object.
453
454        self.header: must be defining with dictionary of headers.
455        self.body: if define then used as request body. None by default.
456        self.timeout: global request timeout, 15 seconds by default.
457        :param url: url with REST request.
458        :param reqType: send "GET" or "POST" request. "GET" by default.
459        :param retry: how many times retry after first request if an 5xx server errors occurred.
460        :param pause: sleep time in seconds between retries.
461        :return: response JSON (dictionary) from broker.
462        """
463        if reqType.upper() not in ("GET", "POST"):
464            uLogger.error("You can define request type: `GET` or `POST`!")
465            raise Exception("Incorrect value")
466
467        if self.moreDebug:
468            uLogger.debug("Request parameters:")
469            uLogger.debug("    - REST API URL: {}".format(url))
470            uLogger.debug("    - request type: {}".format(reqType))
471            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
472            uLogger.debug("    - body:\n{}".format(self.body))
473
474        # fast hack to avoid all operations with some tickers/FIGI
475        responseJSON = {}
476        oK = True
477        for item in self.exclude:
478            if item in url:
479                if self.moreDebug:
480                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
481
482                oK = False
483                break
484
485        if oK:
486            with self.__lock:  # acquire the mutex lock
487                counter = 0
488                response = None
489                errMsg = ""
490
491                while not response and counter <= retry:
492                    if reqType == "GET":
493                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
494
495                    if reqType == "POST":
496                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
497
498                    if self.moreDebug:
499                        uLogger.debug("Response:")
500                        uLogger.debug("    - status code: {}".format(response.status_code))
501                        uLogger.debug("    - reason: {}".format(response.reason))
502                        uLogger.debug("    - body length: {}".format(len(response.text)))
503                        uLogger.debug("    - headers:\n{}".format(response.headers))
504
505                    # Server returns some headers:
506                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
507                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
508                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
509                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
510                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
511                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
512                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
513                        sleep(rateLimitWait)
514
515                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
516                    if 400 <= response.status_code < 500:
517                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
518                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
519
520                        if "code" in response.text and "message" in response.text:
521                            msgDict = self._ParseJSON(rawData=response.text)
522                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
523
524                        counter = retry + 1  # do not retry for 4xx errors
525
526                    if 500 <= response.status_code < 600:
527                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
528                        uLogger.debug("    - not oK, {}".format(errMsg))
529
530                        if "code" in response.text and "message" in response.text:
531                            errMsgDict = self._ParseJSON(rawData=response.text)
532                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
533
534                        counter += 1
535
536                        if counter <= retry:
537                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
538                            sleep(pause)
539
540                responseJSON = self._ParseJSON(rawData=response.text)
541
542                if errMsg:
543                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
544                    uLogger.error("    - not oK, {}".format(errMsg))
545
546        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
579    def Listing(self) -> dict:
580        """
581        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
582
583        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
584        """
585        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
586        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
587
588        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
589        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
590        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
591
592        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
593        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
594        poolUpdater.close()  # close the thread pool
595        poolUpdater.join()  # wait a moment until all data returns from threads
596
597        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
598        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
599        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
600
601        # calculate minimum price increment (step) for all instruments and set up instrument's type:
602        for iType in iList.keys():
603            for ticker in iList[iType]:
604                iList[iType][ticker]["type"] = iType
605
606                if "minPriceIncrement" in iList[iType][ticker].keys():
607                    iList[iType][ticker]["step"] = NanoToFloat(
608                        iList[iType][ticker]["minPriceIncrement"]["units"],
609                        iList[iType][ticker]["minPriceIncrement"]["nano"],
610                    )
611
612                else:
613                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
614
615        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
617    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
618        """
619        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
620
621        See also: `DumpInstruments()`, `Listing()`.
622
623        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
624                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
625        """
626        if self.iListDumpFile is None or not self.iListDumpFile:
627            uLogger.error("Output name of dump file must be defined!")
628            raise Exception("Filename required")
629
630        if not self.iList or forceUpdate:
631            self.iList = self.Listing()
632
633        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
634
635        # Save as XLSX with separated sheets for every type of instruments:
636        with pd.ExcelWriter(
637                path=xlsxDumpFile,
638                date_format=TKS_DATE_FORMAT,
639                datetime_format=TKS_DATE_TIME_FORMAT,
640                mode="w",
641        ) as writer:
642            for iType in TKS_INSTRUMENTS:
643                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
644                df = df[sorted(df)]  # sorted by column names
645                df = df.applymap(
646                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
647                    na_action="ignore",
648                )  # converting numbers from nano-type to float in every cell
649                df.to_excel(
650                    writer,
651                    sheet_name=iType,
652                    encoding="UTF-8",
653                    freeze_panes=(1, 1),
654                )  # saving as XLSX-file with freeze first row and column as headers
655
656        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
658    def DumpInstruments(self, forceUpdate: bool = True) -> str:
659        """
660        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
661        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
662
663        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
664
665        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
666                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
667        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
668        """
669        if self.iListDumpFile is None or not self.iListDumpFile:
670            uLogger.error("Output name of dump file must be defined!")
671            raise Exception("Filename required")
672
673        if not self.iList or forceUpdate:
674            self.iList = self.Listing()
675
676        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
677        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
678            fH.write(jsonDump)
679
680        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
681
682        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
684    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
685        """
686        Show information about one instrument defined by json data and prints it in Markdown format.
687
688        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
689
690        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
691        :param show: if `True` then also printing information about instrument and its current price.
692        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
693        :return: multilines text in Markdown format with information about one instrument.
694        """
695        splitLine = "|                                                             |                                                        |\n"
696        infoText = ""
697
698        if iJSON is not None and iJSON and isinstance(iJSON, dict):
699            info = [
700                "# Main information\n\n",
701                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
702                "| Parameters                                                  | Values                                                 |\n",
703                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
704                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
705                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
706            ]
707
708            if "sector" in iJSON.keys() and iJSON["sector"]:
709                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
710
711            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
712                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
713
714            info.extend([
715                splitLine,
716                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
717                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
718            ])
719
720            if "isin" in iJSON.keys() and iJSON["isin"]:
721                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
722
723            if "classCode" in iJSON.keys():
724                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
725
726            info.extend([
727                splitLine,
728                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
729                splitLine,
730                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
731                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
732                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
733            ])
734
735            if iJSON["figi"]:
736                self._figi = iJSON["figi"]
737                iJSON = iJSON | self.RequestTradingStatus()
738
739                info.extend([
740                    splitLine,
741                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
742                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
743                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
744                ])
745
746            info.append(splitLine)
747
748            if "type" in iJSON.keys() and iJSON["type"]:
749                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
750
751                if "shareType" in iJSON.keys() and iJSON["shareType"]:
752                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
753
754            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
755                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
756
757            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
758                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
759
760            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
761                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
762
763            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
764                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
765
766            if "focusType" in iJSON.keys() and iJSON["focusType"]:
767                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
768
769            if "assetType" in iJSON.keys() and iJSON["assetType"]:
770                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
771
772            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
773                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
774
775            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
776                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
777
778            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
779                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
780
781            if "currency" in iJSON.keys():
782                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
783
784            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
785                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
786
787            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
788                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
789
790            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
791                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
792
793            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
794                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
795
796            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
797                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
798
799            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
800                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
801
802            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
803                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
804
805            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
806                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
807
808            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
809                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
810
811            iExt = None
812            if iJSON["type"] == "Bonds":
813                info.extend([
814                    splitLine,
815                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
816                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
817                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
818                        iJSON["nominal"]["currency"],
819                    )),
820                ])
821
822                if "floatingCouponFlag" in iJSON.keys():
823                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
824
825                if "amortizationFlag" in iJSON.keys():
826                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
827
828                info.append(splitLine)
829
830                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
831                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
832
833                if iJSON["figi"]:
834                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
835
836                    info.extend([
837                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
838                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
839                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
840                    ])
841
842                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
843                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
844                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
845                        iJSON["aciValue"]["currency"]
846                    )))
847
848            if "currentPrice" in iJSON.keys():
849                info.append(splitLine)
850
851                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
852                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
853
854                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
855                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
856                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
857                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
858                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
859
860                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
861                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
862
863                info.extend([
864                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
865                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
866                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
867                    )),
868                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
869                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
870                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
871                    )),
872                    "| Changes between last deal price and last close              | {:<54} |\n".format(
873                        "{:.2f}%{}".format(
874                            iJSON["currentPrice"]["changes"],
875                            " ({}{:.2f} {})".format(
876                                "+" if bondChangesDelta > 0 else "",
877                                bondChangesDelta,
878                                aciCurrency
879                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
880                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
881                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
882                                currency
883                            ),
884                        )
885                    ),
886                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
887                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
888                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
889                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
890                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
891                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
892                    )),
893                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
894                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
895                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
896                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
897                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
898                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
899                    )),
900                ])
901
902            if "lot" in iJSON.keys():
903                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
904
905            if "step" in iJSON.keys() and iJSON["step"] != 0:
906                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
907
908            # Add bond payment calendar:
909            if iJSON["type"] == "Bonds":
910                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
911                info.extend(["\n#", strCalendar])
912
913            infoText += "".join(info)
914
915            if show and not onlyFiles:
916                uLogger.info("{}".format(infoText))
917
918            if self.infoFile is not None and (show or onlyFiles):
919                with open(self.infoFile, "w", encoding="UTF-8") as fH:
920                    fH.write(infoText)
921
922                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
923
924                if self.useHTMLReports:
925                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
926                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
927                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
928
929                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
930
931        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self._ticker]
  • show: if True then also printing information about instrument and its current price.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 933    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 934        """
 935        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 936
 937        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 938        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 939        :return: JSON formatted data with information about instrument.
 940        """
 941        tickerJSON = {}
 942        if self.moreDebug:
 943            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 944
 945        if not self._ticker:
 946            uLogger.warning("self._ticker variable is not be empty!")
 947
 948        else:
 949            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 950                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 951                raise Exception("Instrument not allowed")
 952
 953            if not self.iList:
 954                self.iList = self.Listing()
 955
 956            if self._ticker in self.iList["Shares"].keys():
 957                tickerJSON = self.iList["Shares"][self._ticker]
 958                if self.moreDebug:
 959                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 960
 961            elif self._ticker in self.iList["Currencies"].keys():
 962                tickerJSON = self.iList["Currencies"][self._ticker]
 963                if self.moreDebug:
 964                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 965
 966            elif self._ticker in self.iList["Bonds"].keys():
 967                tickerJSON = self.iList["Bonds"][self._ticker]
 968                if self.moreDebug:
 969                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 970
 971            elif self._ticker in self.iList["Etfs"].keys():
 972                tickerJSON = self.iList["Etfs"][self._ticker]
 973                if self.moreDebug:
 974                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 975
 976            elif self._ticker in self.iList["Futures"].keys():
 977                tickerJSON = self.iList["Futures"][self._ticker]
 978                if self.moreDebug:
 979                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 980
 981        if tickerJSON:
 982            self._figi = tickerJSON["figi"]
 983
 984            if requestPrice:
 985                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 986
 987                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 988                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 989
 990                else:
 991                    tickerJSON["currentPrice"]["changes"] = 0
 992
 993            if show:
 994                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 995
 996        else:
 997            if show:
 998                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 999
1000        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1002    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1003        """
1004        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1005
1006        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1007        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1008        :return: JSON formatted data with information about instrument.
1009        """
1010        figiJSON = {}
1011        if self.moreDebug:
1012            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
1013
1014        if not self._figi:
1015            uLogger.warning("self._figi variable is not be empty!")
1016
1017        else:
1018            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1019                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
1020                raise Exception("Instrument not allowed")
1021
1022            if not self.iList:
1023                self.iList = self.Listing()
1024
1025            for item in self.iList["Shares"].keys():
1026                if self._figi == self.iList["Shares"][item]["figi"]:
1027                    figiJSON = self.iList["Shares"][item]
1028
1029                    if self.moreDebug:
1030                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1031
1032                    break
1033
1034            if not figiJSON:
1035                for item in self.iList["Currencies"].keys():
1036                    if self._figi == self.iList["Currencies"][item]["figi"]:
1037                        figiJSON = self.iList["Currencies"][item]
1038
1039                        if self.moreDebug:
1040                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1041
1042                        break
1043
1044            if not figiJSON:
1045                for item in self.iList["Bonds"].keys():
1046                    if self._figi == self.iList["Bonds"][item]["figi"]:
1047                        figiJSON = self.iList["Bonds"][item]
1048
1049                        if self.moreDebug:
1050                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1051
1052                        break
1053
1054            if not figiJSON:
1055                for item in self.iList["Etfs"].keys():
1056                    if self._figi == self.iList["Etfs"][item]["figi"]:
1057                        figiJSON = self.iList["Etfs"][item]
1058
1059                        if self.moreDebug:
1060                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1061
1062                        break
1063
1064            if not figiJSON:
1065                for item in self.iList["Futures"].keys():
1066                    if self._figi == self.iList["Futures"][item]["figi"]:
1067                        figiJSON = self.iList["Futures"][item]
1068
1069                        if self.moreDebug:
1070                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1071
1072                        break
1073
1074        if figiJSON:
1075            self._figi = figiJSON["figi"]
1076            self._ticker = figiJSON["ticker"]
1077
1078            if requestPrice:
1079                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1080
1081                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1082                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1083
1084                else:
1085                    figiJSON["currentPrice"]["changes"] = 0
1086
1087            if show:
1088                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1089
1090        else:
1091            if show:
1092                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1093
1094        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1096    def GetCurrentPrices(self, show: bool = True) -> dict:
1097        """
1098        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1099        `{"buy": [{"price": 1243.8, "quantity": 193},
1100                  {"price": 1244.0, "quantity": 168},
1101                  {"price": 1244.8, "quantity": 5},
1102                  {"price": 1245.0, "quantity": 61},
1103                  {"price": 1245.4, "quantity": 60}],
1104          "sell": [{"price": 1243.6, "quantity": 8},
1105                   {"price": 1242.6, "quantity": 10},
1106                   {"price": 1242.4, "quantity": 18},
1107                   {"price": 1242.2, "quantity": 50},
1108                   {"price": 1242.0, "quantity": 113}],
1109          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1110        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1111        - sell: list of dicts with Buyers prices,
1112            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1113            - quantity: volume value by current price in lots,
1114        - limitUp: current trade session limit price, maximum,
1115        - limitDown: current trade session limit price, minimum,
1116        - lastPrice: last deal price of the instrument,
1117        - closePrice: previous trade session close price of the instrument.
1118
1119        See also: `SearchByTicker()` and `SearchByFIGI()`.
1120        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1121        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1122
1123        :param show: if `True` then print DOM to log and console.
1124        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1125                 If an error occurred then returns an empty record:
1126                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1127        """
1128        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1129
1130        if self.depth < 1:
1131            uLogger.error("Depth of Market (DOM) must be >=1!")
1132            raise Exception("Incorrect value")
1133
1134        if not (self._ticker or self._figi):
1135            uLogger.error("self._ticker or self._figi variables must be defined!")
1136            raise Exception("Ticker or FIGI required")
1137
1138        if self._ticker and not self._figi:
1139            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1140            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1141
1142        if not self._ticker and self._figi:
1143            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1144            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1145
1146        if not self._figi:
1147            uLogger.error("FIGI is not defined!")
1148            raise Exception("Ticker or FIGI required")
1149
1150        else:
1151            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1152
1153            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1154            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1155            self.body = str({"figi": self._figi, "depth": self.depth})
1156            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1157
1158            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1159                # list of dicts with sellers orders:
1160                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1161
1162                # list of dicts with buyers orders:
1163                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1164
1165                # max price of instrument at this time:
1166                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1167
1168                # min price of instrument at this time:
1169                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1170
1171                # last price of deal with instrument:
1172                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1173
1174                # last close price of instrument:
1175                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1176
1177            else:
1178                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1179                uLogger.debug("Server response: {}".format(pricesResponse))
1180
1181            if show:
1182                if prices["buy"] or prices["sell"]:
1183                    info = [
1184                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1185                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1186                            self._ticker,
1187                            self._figi,
1188                            self.depth,
1189                        ),
1190                        "-" * 60, "\n",
1191                        "             Orders of Buyers | Orders of Sellers\n",
1192                        "-" * 60, "\n",
1193                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1194                        "-" * 60, "\n",
1195                    ]
1196
1197                    if not prices["buy"]:
1198                        info.append("                              | No orders!\n")
1199                        sumBuy = 0
1200
1201                    else:
1202                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1203                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1204                        for item in maxMinSorted:
1205                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1206
1207                    if not prices["sell"]:
1208                        info.append("No orders!                    |\n")
1209                        sumSell = 0
1210
1211                    else:
1212                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1213                        for item in prices["sell"]:
1214                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1215
1216                    info.extend([
1217                        "-" * 60, "\n",
1218                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1219                        "-" * 60, "\n",
1220                    ])
1221
1222                    infoText = "".join(info)
1223
1224                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1225
1226                else:
1227                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1228
1229        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1231    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1232        """
1233        This method get and show information about all available broker instruments for current user account.
1234        If `instrumentsFile` string is not empty then also save information to this file.
1235
1236        :param show: if `True` then print results to console, if `False` — print only to file.
1237        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1238        :return: multi-lines string with all available broker instruments.
1239        """
1240        if not self.iList:
1241            self.iList = self.Listing()
1242
1243        info = [
1244            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1245            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1246        ]
1247
1248        # add instruments count by type:
1249        for iType in self.iList.keys():
1250            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1251
1252        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1253        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1254
1255        # generating info tables with all instruments by type:
1256        for iType in self.iList.keys():
1257            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1258
1259            for instrument in self.iList[iType].keys():
1260                iName = self.iList[iType][instrument]["name"]  # instrument's name
1261                if len(iName) > 57:
1262                    iName = "{}...".format(iName[:54])  # right trim for a long string
1263
1264                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1265                    self.iList[iType][instrument]["ticker"],
1266                    iName,
1267                    self.iList[iType][instrument]["figi"],
1268                    self.iList[iType][instrument]["currency"],
1269                    self.iList[iType][instrument]["lot"],
1270                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1271                ))
1272
1273        infoText = "".join(info)
1274
1275        if show and not onlyFiles:
1276            uLogger.info(infoText)
1277
1278        if self.instrumentsFile and (show or onlyFiles):
1279            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1280                fH.write(infoText)
1281
1282            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1283
1284            if self.useHTMLReports:
1285                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1286                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1287                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1288
1289                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1290
1291        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multi-lines string with all available broker instruments.

def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1293    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1294        """
1295        This method search and show information about instruments by part of its ticker, FIGI or name.
1296        If `searchResultsFile` string is not empty then also save information to this file.
1297
1298        :param pattern: string with part of ticker, FIGI or instrument's name.
1299        :param show: if `True` then print results to console, if `False` — return list of result only.
1300        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1301        :return: list of dictionaries with all found instruments.
1302        """
1303        if not self.iList:
1304            self.iList = self.Listing()
1305
1306        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1307        compiledPattern = re.compile(pattern, re.IGNORECASE)
1308
1309        for iType in self.iList:
1310            for instrument in self.iList[iType].values():
1311                searchResult = compiledPattern.search(" ".join(
1312                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1313                ))
1314
1315                if searchResult:
1316                    searchResults[iType][instrument["ticker"]] = instrument
1317
1318        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1319        info = [
1320            "# Search results\n\n",
1321            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1322            "* **Search pattern:** [{}]\n".format(pattern),
1323            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1324            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1325        ]
1326        infoShort = info[:]
1327
1328        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1329        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1330        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1331
1332        if resultsLen == 0:
1333            info.append("\nNo results\n")
1334            infoShort.append("\nNo results\n")
1335            uLogger.warning("No results. Try changing your search pattern.")
1336
1337        else:
1338            for iType in searchResults:
1339                iTypeValuesCount = len(searchResults[iType].values())
1340                if iTypeValuesCount > 0:
1341                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1342                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1343
1344                    for instrument in searchResults[iType].values():
1345                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1346                            instrument["type"],
1347                            instrument["ticker"],
1348                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1349                            instrument["figi"],
1350                        ))
1351
1352                    if iTypeValuesCount <= 5:
1353                        infoShort.extend(info[-iTypeValuesCount:])
1354
1355                    else:
1356                        infoShort.extend(info[-5:])
1357                        infoShort.append(skippedLine)
1358
1359        infoText = "".join(info)
1360        infoTextShort = "".join(infoShort)
1361
1362        if show and not onlyFiles:
1363            uLogger.info(infoTextShort)
1364            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1365
1366        if self.searchResultsFile and (show or onlyFiles):
1367            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1368                fH.write(infoText)
1369
1370            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1371
1372            if self.useHTMLReports:
1373                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1374                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1375                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1376
1377                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1378
1379        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1381    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1382        """
1383        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1384
1385        :param instruments: list of strings with tickers or FIGIs.
1386        :return: list with unique instrument FIGIs only.
1387        """
1388        requestedInstruments = []
1389        for iName in instruments:
1390            if iName not in self.aliases.keys():
1391                if iName not in requestedInstruments:
1392                    requestedInstruments.append(iName)
1393
1394            else:
1395                if iName not in requestedInstruments:
1396                    if self.aliases[iName] not in requestedInstruments:
1397                        requestedInstruments.append(self.aliases[iName])
1398
1399        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1400
1401        onlyUniqueFIGIs = []
1402        for iName in requestedInstruments:
1403            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1404                continue
1405
1406            self._ticker = iName
1407            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1408
1409            if not iData:
1410                self._ticker = ""
1411                self._figi = iName
1412
1413                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1414
1415                if not iData:
1416                    self._figi = ""
1417                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1418
1419            if iData and iData["figi"] not in onlyUniqueFIGIs:
1420                onlyUniqueFIGIs.append(iData["figi"])
1421
1422        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1423
1424        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices( self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1426    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1427        """
1428        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1429
1430        See limits: https://tinkoff.github.io/investAPI/limits/
1431
1432        If `pricesFile` string is not empty then also save information to this file.
1433
1434        :param instruments: list of strings with tickers or FIGIs.
1435        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1436        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1437        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1438                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1439        """
1440        if instruments is None or not instruments:
1441            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1442            raise Exception("Ticker or FIGI required")
1443
1444        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1445
1446        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1447
1448        iList = []  # trying to get info and current prices about all unique instruments:
1449        for self._figi in onlyUniqueFIGIs:
1450            iData = self.SearchByFIGI(requestPrice=True, show=False)
1451            iList.append(iData)
1452
1453        self.ShowListOfPrices(iList, show, onlyFiles)
1454
1455        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1457    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1458        """
1459        Show table contains current prices of given instruments.
1460
1461        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1462                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1463        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1464        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1465        :return: multilines text in Markdown format as a table contains current prices.
1466        """
1467        infoText = ""
1468
1469        if show or self.pricesFile or onlyFiles:
1470            info = [
1471                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1472                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1473                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1474            ]
1475
1476            for item in iList:
1477                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1478                    item["ticker"],
1479                    item["figi"],
1480                    item["type"],
1481                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1482                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1483                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1484                    "{} / {}".format(
1485                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1486                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1487                    ),
1488                    "{} / {}".format(
1489                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1490                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1491                    ),
1492                    item["currency"],
1493                ))
1494
1495            infoText = "".join(info)
1496
1497            if show and not onlyFiles:
1498                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1499
1500            if self.pricesFile and (show or onlyFiles):
1501                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1502                    fH.write(infoText)
1503
1504                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1505
1506                if self.useHTMLReports:
1507                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1508                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1509                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1510
1511                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1512
1513        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1515    def RequestTradingStatus(self) -> dict:
1516        """
1517        Requesting trading status for the instrument defined by `figi` variable.
1518
1519        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1520
1521        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1522
1523        :return: dictionary with trading status attributes. Response example:
1524                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1525                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1526        """
1527        if self._figi is None or not self._figi:
1528            uLogger.error("Variable `figi` must be defined for using this method!")
1529            raise Exception("FIGI required")
1530
1531        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1532
1533        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1534        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1535        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1536
1537        if self.moreDebug:
1538            uLogger.debug("Records about current trading status successfully received")
1539
1540        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1542    def RequestPortfolio(self) -> dict:
1543        """
1544        Requesting actual user's portfolio for current `accountId`.
1545
1546        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1547
1548        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1549
1550        :return: dictionary with user's portfolio.
1551        """
1552        if self.accountId is None or not self.accountId:
1553            uLogger.error("Variable `accountId` must be defined for using this method!")
1554            raise Exception("Account ID required")
1555
1556        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1557
1558        self.body = str({"accountId": self.accountId})
1559        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1560        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1561
1562        if self.moreDebug:
1563            uLogger.debug("Records about user's portfolio successfully received")
1564
1565        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1567    def RequestPositions(self) -> dict:
1568        """
1569        Requesting open positions by currencies and instruments for current `accountId`.
1570
1571        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1572
1573        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1574
1575        :return: dictionary with open positions by instruments.
1576        """
1577        if self.accountId is None or not self.accountId:
1578            uLogger.error("Variable `accountId` must be defined for using this method!")
1579            raise Exception("Account ID required")
1580
1581        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1582
1583        self.body = str({"accountId": self.accountId})
1584        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1585        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1586
1587        if self.moreDebug:
1588            uLogger.debug("Records about current open positions successfully received")
1589
1590        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1592    def RequestPendingOrders(self) -> list:
1593        """
1594        Requesting current actual pending limit orders for current `accountId`.
1595
1596        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1597
1598        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1599
1600        :return: list of dictionaries with pending limit orders.
1601        """
1602        if self.accountId is None or not self.accountId:
1603            uLogger.error("Variable `accountId` must be defined for using this method!")
1604            raise Exception("Account ID required")
1605
1606        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1607
1608        self.body = str({"accountId": self.accountId})
1609        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1610        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1611
1612        if "orders" in rawResponse.keys():
1613            rawOrders = rawResponse["orders"]
1614            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1615
1616        else:
1617            rawOrders = []
1618            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1619
1620        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1622    def RequestStopOrders(self) -> list:
1623        """
1624        Requesting current actual stop orders for current `accountId`.
1625
1626        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1627
1628        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1629
1630        :return: list of dictionaries with stop orders.
1631        """
1632        if self.accountId is None or not self.accountId:
1633            uLogger.error("Variable `accountId` must be defined for using this method!")
1634            raise Exception("Account ID required")
1635
1636        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1637
1638        self.body = str({"accountId": self.accountId})
1639        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1640        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1641
1642        if "stopOrders" in rawResponse.keys():
1643            rawStopOrders = rawResponse["stopOrders"]
1644            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1645
1646        else:
1647            rawStopOrders = []
1648            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1649
1650        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full', onlyFiles=False) -> dict:
1652    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1653        """
1654        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1655        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1656        and `overviewBondsCalendarFile` are defined then also save information to file.
1657
1658        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1659        many requests about the state of the portfolio, and then, based on the received data, a large number
1660        of calculation and statistics are collected.
1661
1662        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1663        :param details: how detailed should the information be?
1664        - `full` — shows full available information about portfolio status (by default),
1665        - `positions` — shows only open positions,
1666        - `orders` — shows only sections of open limits and stop orders.
1667        - `digest` — show a short digest of the portfolio status,
1668        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1669        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1670        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1671        :return: dictionary with client's raw portfolio and some statistics.
1672        """
1673        if self.accountId is None or not self.accountId:
1674            uLogger.error("Variable `accountId` must be defined for using this method!")
1675            raise Exception("Account ID required")
1676
1677        view = {
1678            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1679                "headers": {},  # list of dictionaries, response headers without "positions" section
1680                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1681                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1682                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1683                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1684                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1685                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1686                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1687                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1688                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1689            },
1690            "stat": {  # --- some statistics calculated using "raw" sections:
1691                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1692                "availableRUB": 0.,  # available rubles (without other currencies)
1693                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1694                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1695                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1696                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1697                "sharesCostRUB": 0.,  # costs of all shares in RUB
1698                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1699                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1700                "futuresCostRUB": 0.,  # costs of all futures in RUB
1701                "Currencies": [],  # list of dictionaries of all currencies statistics
1702                "Shares": [],  # list of dictionaries of all shares statistics
1703                "Bonds": [],  # list of dictionaries of all bonds statistics
1704                "Etfs": [],  # list of dictionaries of all etfs statistics
1705                "Futures": [],  # list of dictionaries of all futures statistics
1706                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1707                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1708                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1709                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1710                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1711            },
1712            "analytics": {  # --- some analytics of portfolio:
1713                "distrByAssets": {},  # portfolio distribution by assets
1714                "distrByCompanies": {},  # portfolio distribution by companies
1715                "distrBySectors": {},  # portfolio distribution by sectors
1716                "distrByCurrencies": {},  # portfolio distribution by currencies
1717                "distrByCountries": {},  # portfolio distribution by countries
1718                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1719            }
1720        }
1721
1722        details = details.lower()
1723        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1724        if details not in availableDetails:
1725            details = "full"
1726            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1727
1728        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1729
1730        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1731        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1732        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1733        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1734
1735        # save response headers without "positions" section:
1736        for key in portfolioResponse.keys():
1737            if key != "positions":
1738                view["raw"]["headers"][key] = portfolioResponse[key]
1739
1740            else:
1741                continue
1742
1743        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1744        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1745        for item in portfolioResponse["positions"]:
1746            if item["instrumentType"] == "currency":
1747                self._figi = item["figi"]
1748                if not self._figi and item["ticker"]:
1749                    self._ticker = item["ticker"]
1750                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1751
1752                curr = self.SearchByFIGI(requestPrice=False)
1753
1754                # current price of currency in RUB:
1755                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1756                    "name": curr["name"],
1757                    "currentPrice": NanoToFloat(
1758                        item["currentPrice"]["units"],
1759                        item["currentPrice"]["nano"]
1760                    ),
1761                }
1762
1763                view["raw"]["Currencies"].append(item)
1764
1765            elif item["instrumentType"] == "share":
1766                view["raw"]["Shares"].append(item)
1767
1768            elif item["instrumentType"] == "bond":
1769                view["raw"]["Bonds"].append(item)
1770
1771            elif item["instrumentType"] == "etf":
1772                view["raw"]["Etfs"].append(item)
1773
1774            elif item["instrumentType"] == "futures":
1775                view["raw"]["Futures"].append(item)
1776
1777            else:
1778                continue
1779
1780        # how many volume of currencies (by ISO currency name) are blocked:
1781        for item in view["raw"]["positions"]["blocked"]:
1782            blocked = NanoToFloat(item["units"], item["nano"])
1783            if blocked > 0:
1784                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1785
1786        # how many volume of instruments (by FIGI) are blocked:
1787        for item in view["raw"]["positions"]["securities"]:
1788            blocked = int(item["blocked"])
1789            if blocked > 0:
1790                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1791
1792        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1793
1794        if "rub" in allBlocked.keys():
1795            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1796
1797        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1798        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1799        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1800        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1801        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1802        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1803        view["stat"]["portfolioCostRUB"] = sum([
1804            view["stat"]["allCurrenciesCostRUB"],
1805            view["stat"]["sharesCostRUB"],
1806            view["stat"]["bondsCostRUB"],
1807            view["stat"]["etfsCostRUB"],
1808            view["stat"]["futuresCostRUB"],
1809        ])
1810
1811        # --- calculating some portfolio statistics:
1812        byComp = {}  # distribution by companies
1813        bySect = {}  # distribution by sectors
1814        byCurr = {}  # distribution by currencies (include RUB)
1815        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1816        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1817
1818        for item in portfolioResponse["positions"]:
1819            self._figi = item["figi"]
1820            if not self._figi and item["ticker"]:
1821                self._ticker = item["ticker"]
1822                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1823
1824            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1825
1826            if instrument:
1827                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1828                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1829
1830                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1831                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1832
1833                else:
1834                    blocked = 0
1835
1836                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1837                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1838                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1839                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1840                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1841                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1842                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1843                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1844                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1845                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1846                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1847                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1848
1849                statData = {
1850                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1851                    "ticker": instrument["ticker"],  # ticker by FIGI
1852                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1853                    "volume": volume,  # available volume of instrument
1854                    "lots": lots,  # volume in lots of instrument
1855                    "direction": direction,  # direction of an instrument's position: short or long
1856                    "blocked": blocked,  # blocked volume of currency or instrument
1857                    "currentPrice": curPrice,  # current instrument's price in basic asset
1858                    "average": average,  # current average position price
1859                    "cost": cost,  # current cost of all volume of instrument in basic asset
1860                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1861                    "costRUB": costRUB,  # cost of instrument in ruble
1862                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1863                    "profit": profit,  # expected profit at current moment
1864                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1865                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1866                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1867                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1868                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1869                    "step": instrument["step"],  # minimum price increment
1870                }
1871
1872                # adding distribution by unique countries:
1873                if statData["country"] not in byCountry.keys():
1874                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1875
1876                else:
1877                    byCountry[statData["country"]]["cost"] += costRUB
1878                    byCountry[statData["country"]]["percent"] += percentCostRUB
1879
1880                if item["instrumentType"] != "currency":
1881                    # adding distribution by unique companies:
1882                    if statData["name"]:
1883                        if statData["name"] not in byComp.keys():
1884                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1885
1886                        else:
1887                            byComp[statData["name"]]["cost"] += costRUB
1888                            byComp[statData["name"]]["percent"] += percentCostRUB
1889
1890                    # adding distribution by unique sectors:
1891                    if statData["sector"] not in bySect.keys():
1892                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1893
1894                    else:
1895                        bySect[statData["sector"]]["cost"] += costRUB
1896                        bySect[statData["sector"]]["percent"] += percentCostRUB
1897
1898                # adding distribution by unique currencies:
1899                if currency not in byCurr.keys():
1900                    byCurr[currency] = {
1901                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1902                        "cost": costRUB,
1903                        "percent": percentCostRUB
1904                    }
1905
1906                else:
1907                    byCurr[currency]["cost"] += costRUB
1908                    byCurr[currency]["percent"] += percentCostRUB
1909
1910                # saving statistics for every instrument:
1911                if item["instrumentType"] == "currency":
1912                    view["stat"]["Currencies"].append(statData)
1913
1914                    # update dict with free funds for trading (total - blocked) by currencies
1915                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1916                    view["stat"]["funds"][currency] = {
1917                        "total": volume,
1918                        "totalCostRUB": costRUB,  # total volume cost in rubles
1919                        "free": volume - blocked,
1920                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1921                    }
1922
1923                elif item["instrumentType"] == "share":
1924                    view["stat"]["Shares"].append(statData)
1925
1926                elif item["instrumentType"] == "bond":
1927                    view["stat"]["Bonds"].append(statData)
1928
1929                elif item["instrumentType"] == "etf":
1930                    view["stat"]["Etfs"].append(statData)
1931
1932                elif item["instrumentType"] == "Futures":
1933                    view["stat"]["Futures"].append(statData)
1934
1935                else:
1936                    continue
1937
1938        # total changes in Russian Ruble:
1939        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1940        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1941        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1942        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1943        view["stat"]["funds"]["rub"] = {
1944            "total": view["stat"]["availableRUB"],
1945            "totalCostRUB": view["stat"]["availableRUB"],
1946            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1947            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1948        }
1949
1950        # --- pending limit orders sector data:
1951        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1952        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1953
1954        for item in view["raw"]["orders"]:
1955            self._figi = item["figi"]
1956
1957            if item["figi"] not in uniquePendingOrdersFIGIs:
1958                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1959
1960                uniquePendingOrdersFIGIs.append(item["figi"])
1961                uniquePendingOrders[item["figi"]] = instrument
1962
1963            else:
1964                instrument = uniquePendingOrders[item["figi"]]
1965
1966            if instrument:
1967                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1968                orderType = TKS_ORDER_TYPES[item["orderType"]]
1969                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1970                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1971
1972                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1973                if item["direction"] == "ORDER_DIRECTION_BUY":
1974                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1975
1976                else:
1977                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1978
1979                # requested price for order execution:
1980                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1981
1982                # necessary changes in percent to reach target from current price:
1983                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1984
1985                view["stat"]["orders"].append({
1986                    "orderID": item["orderId"],  # orderId number parameter of current order
1987                    "figi": item["figi"],  # FIGI identification
1988                    "ticker": instrument["ticker"],  # ticker name by FIGI
1989                    "lotsRequested": item["lotsRequested"],  # requested lots value
1990                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1991                    "currentPrice": lastPrice,  # current instrument's price for defined action
1992                    "targetPrice": target,  # requested price for order execution in base currency
1993                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1994                    "percentChanges": changes,  # changes in percent to target from current price
1995                    "currency": item["currency"],  # instrument's currency name
1996                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1997                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1998                    "status": orderState,  # order status from TKS_ORDER_STATES
1999                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
2000                })
2001
2002        # --- stop orders sector data:
2003        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
2004        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
2005
2006        for item in view["raw"]["stopOrders"]:
2007            self._figi = item["figi"]
2008
2009            if item["figi"] not in uniqueStopOrdersFIGIs:
2010                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
2011
2012                uniqueStopOrdersFIGIs.append(item["figi"])
2013                uniqueStopOrders[item["figi"]] = instrument
2014
2015            else:
2016                instrument = uniqueStopOrders[item["figi"]]
2017
2018            if instrument:
2019                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
2020                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
2021                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
2022
2023                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
2024                if "expirationTime" in item.keys():
2025                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
2026                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
2027
2028                else:
2029                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
2030                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2031
2032                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2033                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2034                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2035
2036                else:
2037                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2038
2039                # requested price when stop-order executed:
2040                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2041
2042                # price for limit-order, set up when stop-order executed:
2043                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2044
2045                # necessary changes in percent to reach target from current price:
2046                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2047
2048                view["stat"]["stopOrders"].append({
2049                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2050                    "figi": item["figi"],  # FIGI identification
2051                    "ticker": instrument["ticker"],  # ticker name by FIGI
2052                    "lotsRequested": item["lotsRequested"],  # requested lots value
2053                    "currentPrice": lastPrice,  # current instrument's price for defined action
2054                    "targetPrice": target,  # requested price for stop-order execution in base currency
2055                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2056                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2057                    "percentChanges": changes,  # changes in percent to target from current price
2058                    "currency": item["currency"],  # instrument's currency name
2059                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2060                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2061                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2062                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2063                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2064                })
2065
2066        # --- calculating data for analytics section:
2067        # portfolio distribution by assets:
2068        view["analytics"]["distrByAssets"] = {
2069            "Ruble": {
2070                "uniques": 1,
2071                "cost": view["stat"]["availableRUB"],
2072                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2073            },
2074            "Currencies": {
2075                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2076                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2077                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2078            },
2079            "Shares": {
2080                "uniques": len(view["stat"]["Shares"]),
2081                "cost": view["stat"]["sharesCostRUB"],
2082                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2083            },
2084            "Bonds": {
2085                "uniques": len(view["stat"]["Bonds"]),
2086                "cost": view["stat"]["bondsCostRUB"],
2087                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2088            },
2089            "Etfs": {
2090                "uniques": len(view["stat"]["Etfs"]),
2091                "cost": view["stat"]["etfsCostRUB"],
2092                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2093            },
2094            "Futures": {
2095                "uniques": len(view["stat"]["Futures"]),
2096                "cost": view["stat"]["futuresCostRUB"],
2097                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2098            },
2099        }
2100
2101        # portfolio distribution by companies:
2102        view["analytics"]["distrByCompanies"]["All money cash"] = {
2103            "ticker": "",
2104            "cost": view["stat"]["allCurrenciesCostRUB"],
2105            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2106        }
2107        view["analytics"]["distrByCompanies"].update(byComp)
2108
2109        # portfolio distribution by sectors:
2110        view["analytics"]["distrBySectors"]["All money cash"] = {
2111            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2112            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2113        }
2114        view["analytics"]["distrBySectors"].update(bySect)
2115
2116        # portfolio distribution by currencies:
2117        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2118            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2119
2120            if self.moreDebug:
2121                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2122
2123        view["analytics"]["distrByCurrencies"].update(byCurr)
2124        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2125        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2126
2127        # portfolio distribution by countries:
2128        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2129            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2130
2131            if self.moreDebug:
2132                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2133
2134        view["analytics"]["distrByCountries"].update(byCountry)
2135        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2136        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2137
2138        # --- Prepare text statistics overview in human-readable:
2139        if show or onlyFiles:
2140            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2141
2142            # Whatever the value `details`, header not changes:
2143            info = [
2144                "# Client's portfolio\n\n",
2145                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2146                "* **Account ID:** [{}]\n".format(self.accountId),
2147            ]
2148
2149            if details in ["full", "positions", "digest"]:
2150                info.extend([
2151                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2152                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2153                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2154                        view["stat"]["totalChangesRUB"],
2155                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2156                        view["stat"]["totalChangesPercentRUB"],
2157                    ),
2158                ])
2159
2160            if details in ["full", "positions"]:
2161                info.extend([
2162                    "## Open positions\n\n",
2163                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2164                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2165                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2166                        "{:.2f} ({:.2f}) rub".format(
2167                            view["stat"]["availableRUB"],
2168                            view["stat"]["blockedRUB"],
2169                        )
2170                    )
2171                ])
2172
2173                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2174                    return [
2175                        "|                             |                                 |          |              |              |                     |                              |\n",
2176                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2177                            noTradeStr if noTradeStr else typeStr,
2178                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2179                        ),
2180                    ]
2181
2182                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2183                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2184                        "{} [{}]".format(data["ticker"], data["figi"]),
2185                        "{:.2f} ({:.2f}) {}".format(
2186                            data["volume"],
2187                            data["blocked"],
2188                            data["currency"],
2189                        ) if isCurr else "{:.0f} ({:.0f})".format(
2190                            data["volume"],
2191                            data["blocked"],
2192                        ),
2193                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2194                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2195                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2196                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2197                        "{}{:.2f} {} ({}{:.2f}%)".format(
2198                            "+" if data["profit"] > 0 else "",
2199                            data["profit"], data["baseCurrencyName"],
2200                            "+" if data["percentProfit"] > 0 else "",
2201                            data["percentProfit"],
2202                        ),
2203                    )
2204
2205                # --- Show currencies section:
2206                if view["stat"]["Currencies"]:
2207                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2208                    for item in view["stat"]["Currencies"]:
2209                        info.append(_InfoStr(item, isCurr=True))
2210
2211                else:
2212                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2213
2214                # --- Show shares section:
2215                if view["stat"]["Shares"]:
2216                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2217
2218                    for item in view["stat"]["Shares"]:
2219                        info.append(_InfoStr(item))
2220
2221                else:
2222                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2223
2224                # --- Show bonds section:
2225                if view["stat"]["Bonds"]:
2226                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2227
2228                    for item in view["stat"]["Bonds"]:
2229                        info.append(_InfoStr(item))
2230
2231                else:
2232                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2233
2234                # --- Show etfs section:
2235                if view["stat"]["Etfs"]:
2236                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2237
2238                    for item in view["stat"]["Etfs"]:
2239                        info.append(_InfoStr(item))
2240
2241                else:
2242                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2243
2244                # --- Show futures section:
2245                if view["stat"]["Futures"]:
2246                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2247
2248                    for item in view["stat"]["Futures"]:
2249                        info.append(_InfoStr(item))
2250
2251                else:
2252                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2253
2254            if details in ["full", "orders"]:
2255                # --- Show pending limit orders section:
2256                if view["stat"]["orders"]:
2257                    info.extend([
2258                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2259                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2260                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2261                    ])
2262
2263                    for item in view["stat"]["orders"]:
2264                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2265                            "{} [{}]".format(item["ticker"], item["figi"]),
2266                            item["orderID"],
2267                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2268                            "{} {} ({}{:.2f}%)".format(
2269                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2270                                item["baseCurrencyName"],
2271                                "+" if item["percentChanges"] > 0 else "",
2272                                float(item["percentChanges"]),
2273                            ),
2274                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2275                            item["action"],
2276                            item["type"],
2277                            item["date"],
2278                        ))
2279
2280                else:
2281                    info.append("\n## Total pending limit-orders: [0]\n")
2282
2283                # --- Show stop orders section:
2284                if view["stat"]["stopOrders"]:
2285                    info.extend([
2286                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2287                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2288                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2289                    ])
2290
2291                    for item in view["stat"]["stopOrders"]:
2292                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2293                            "{} [{}]".format(item["ticker"], item["figi"]),
2294                            item["orderID"],
2295                            item["lotsRequested"],
2296                            "{} {} ({}{:.2f}%)".format(
2297                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2298                                item["baseCurrencyName"],
2299                                "+" if item["percentChanges"] > 0 else "",
2300                                float(item["percentChanges"]),
2301                            ),
2302                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2303                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2304                            item["action"],
2305                            item["type"],
2306                            item["expType"],
2307                            item["createDate"],
2308                            item["expDate"],
2309                        ))
2310
2311                else:
2312                    info.append("\n## Total stop-orders: [0]\n")
2313
2314            if details in ["full", "analytics"]:
2315                # -- Show analytics section:
2316                if view["stat"]["portfolioCostRUB"] > 0:
2317                    info.extend([
2318                        "\n# Analytics\n\n"
2319                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2320                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2321                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2322                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2323                            view["stat"]["totalChangesRUB"],
2324                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2325                            view["stat"]["totalChangesPercentRUB"],
2326                        ),
2327                        "\n## Portfolio distribution by assets\n"
2328                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2329                        "|------------------------------------|---------|---------|--------------------|\n",
2330                    ])
2331
2332                    for key in view["analytics"]["distrByAssets"].keys():
2333                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2334                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2335                                key,
2336                                view["analytics"]["distrByAssets"][key]["uniques"],
2337                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2338                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2339                            ))
2340
2341                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2342
2343                    info.extend([
2344                        "\n## Portfolio distribution by companies\n"
2345                        "\n| Company                                      | Percent | Current cost       |\n",
2346                        aSepLine,
2347                    ])
2348
2349                    for company in view["analytics"]["distrByCompanies"].keys():
2350                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2351                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2352                                "{}{}".format(
2353                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2354                                    company,
2355                                ),
2356                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2357                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2358                            ))
2359
2360                    info.extend([
2361                        "\n## Portfolio distribution by sectors\n"
2362                        "\n| Sector                                       | Percent | Current cost       |\n",
2363                        aSepLine,
2364                    ])
2365
2366                    for sector in view["analytics"]["distrBySectors"].keys():
2367                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2368                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2369                                sector,
2370                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2371                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2372                            ))
2373
2374                    info.extend([
2375                        "\n## Portfolio distribution by currencies\n"
2376                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2377                        aSepLine,
2378                    ])
2379
2380                    for curr in view["analytics"]["distrByCurrencies"].keys():
2381                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2382                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2383                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2384                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2385                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2386                            ))
2387
2388                    info.extend([
2389                        "\n## Portfolio distribution by countries\n"
2390                        "\n| Assets by country                            | Percent | Current cost       |\n",
2391                        aSepLine,
2392                    ])
2393
2394                    for country in view["analytics"]["distrByCountries"].keys():
2395                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2396                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2397                                country,
2398                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2399                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2400                            ))
2401
2402            if details in ["full", "calendar"]:
2403                # -- Show bonds payment calendar section:
2404                if view["stat"]["Bonds"]:
2405                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2406                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2407                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2408
2409                else:
2410                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2411
2412            infoText = "".join(info)
2413
2414            if show and not onlyFiles:
2415                uLogger.info(infoText)
2416
2417            if details == "full" and self.overviewFile:
2418                filename = self.overviewFile
2419
2420            elif details == "digest" and self.overviewDigestFile:
2421                filename = self.overviewDigestFile
2422
2423            elif details == "positions" and self.overviewPositionsFile:
2424                filename = self.overviewPositionsFile
2425
2426            elif details == "orders" and self.overviewOrdersFile:
2427                filename = self.overviewOrdersFile
2428
2429            elif details == "analytics" and self.overviewAnalyticsFile:
2430                filename = self.overviewAnalyticsFile
2431
2432            elif details == "calendar" and self.overviewBondsCalendarFile:
2433                filename = self.overviewBondsCalendarFile
2434
2435            else:
2436                filename = ""
2437
2438            if filename and (show or onlyFiles):
2439                with open(filename, "w", encoding="UTF-8") as fH:
2440                    fH.write(infoText)
2441
2442                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2443
2444                if self.useHTMLReports:
2445                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2446                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2447                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2448
2449                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2450
2451        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio).
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2453    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2454        """
2455        Returns history operations between two given dates for current `accountId`.
2456        If `reportFile` string is not empty then also save human-readable report.
2457        Shows some statistical data of closed positions.
2458
2459        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2460        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2461        :param show: if `True` then also prints all records to the console.
2462        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2463        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2464        :return: original list of dictionaries with history of deals records from API ("operations" key):
2465                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2466                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2467        """
2468        if self.accountId is None or not self.accountId:
2469            uLogger.error("Variable `accountId` must be defined for using this method!")
2470            raise Exception("Account ID required")
2471
2472        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2473
2474        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2475
2476        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2477        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2478        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2479        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2480        customStat = {}  # custom statistics in additional to responseJSON
2481
2482        # --- output report in human-readable format:
2483        if self.reportFile and (show or onlyFiles):
2484            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2485            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2486            nextDay = ""
2487
2488            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2489
2490            if len(ops) > 0:
2491                customStat = {
2492                    "opsCount": 0,  # total operations count
2493                    "buyCount": 0,  # buy operations
2494                    "sellCount": 0,  # sell operations
2495                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2496                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2497                    "payIn": {"rub": 0.},  # Deposit brokerage account
2498                    "payOut": {"rub": 0.},  # Withdrawals
2499                    "divs": {"rub": 0.},  # Dividends income
2500                    "coupons": {"rub": 0.},  # Coupon's income
2501                    "brokerCom": {"rub": 0.},  # Service commissions
2502                    "serviceCom": {"rub": 0.},  # Service commissions
2503                    "marginCom": {"rub": 0.},  # Margin commissions
2504                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2505                }
2506
2507                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2508                for item in ops:
2509                    if item["state"] == "OPERATION_STATE_EXECUTED":
2510                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2511
2512                        # count buy operations:
2513                        if "_BUY" in item["operationType"]:
2514                            customStat["buyCount"] += 1
2515
2516                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2517                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2518
2519                            else:
2520                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2521
2522                        # count sell operations:
2523                        elif "_SELL" in item["operationType"]:
2524                            customStat["sellCount"] += 1
2525
2526                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2527                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2528
2529                            else:
2530                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2531
2532                        # count incoming operations:
2533                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2534                            if item["payment"]["currency"] in customStat["payIn"].keys():
2535                                customStat["payIn"][item["payment"]["currency"]] += payment
2536
2537                            else:
2538                                customStat["payIn"][item["payment"]["currency"]] = payment
2539
2540                        # count withdrawals operations:
2541                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2542                            if item["payment"]["currency"] in customStat["payOut"].keys():
2543                                customStat["payOut"][item["payment"]["currency"]] += payment
2544
2545                            else:
2546                                customStat["payOut"][item["payment"]["currency"]] = payment
2547
2548                        # count dividends income:
2549                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2550                            if item["payment"]["currency"] in customStat["divs"].keys():
2551                                customStat["divs"][item["payment"]["currency"]] += payment
2552
2553                            else:
2554                                customStat["divs"][item["payment"]["currency"]] = payment
2555
2556                        # count coupon's income:
2557                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2558                            if item["payment"]["currency"] in customStat["coupons"].keys():
2559                                customStat["coupons"][item["payment"]["currency"]] += payment
2560
2561                            else:
2562                                customStat["coupons"][item["payment"]["currency"]] = payment
2563
2564                        # count broker commissions:
2565                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2566                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2567                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2568
2569                            else:
2570                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2571
2572                        # count service commissions:
2573                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2574                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2575                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2576
2577                            else:
2578                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2579
2580                        # count margin commissions:
2581                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2582                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2583                                customStat["marginCom"][item["payment"]["currency"]] += payment
2584
2585                            else:
2586                                customStat["marginCom"][item["payment"]["currency"]] = payment
2587
2588                        # count withholding taxes:
2589                        elif "_TAX" in item["operationType"]:
2590                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2591                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2592
2593                            else:
2594                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2595
2596                        else:
2597                            continue
2598
2599                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2600
2601                # --- view "Actions" lines:
2602                info.extend([
2603                    "| Report sections            |                               |                              |                      |                        |\n",
2604                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2605                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2606                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2607                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2608                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2609                    ),
2610                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2611                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2612                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2613                    ),
2614                ])
2615
2616                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2617                for key in opsKeys:
2618                    if key == "rub":
2619                        continue
2620
2621                    info.extend([
2622                        "|                            |                               | {:<28} |                      |                        |\n".format(
2623                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2624                        ),
2625                        "|                            |                               | {:<28} |                      |                        |\n".format(
2626                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2627                        ),
2628                    ])
2629
2630                info.append(splitLine1)
2631
2632                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2633                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2634                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2635                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2636                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2637                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2638                    )
2639
2640                # --- view "Payments" lines:
2641                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2642                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2643
2644                for key in paymentsKeys:
2645                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2646
2647                info.append(splitLine1)
2648
2649                # --- view "Commissions and taxes" lines:
2650                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2651                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2652
2653                for key in comKeys:
2654                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2655
2656                info.extend([
2657                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2658                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2659                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2660                ])
2661
2662            else:
2663                info.append("Broker returned no operations during this period\n")
2664
2665            # --- view "Operations" section:
2666            for item in ops:
2667                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2668                    continue
2669
2670                else:
2671                    self._figi = item["figi"]
2672                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2673                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2674
2675                    # group of deals during one day:
2676                    if nextDay and item["date"].split("T")[0] != nextDay:
2677                        info.append(splitLine2)
2678                        nextDay = ""
2679
2680                    else:
2681                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2682
2683                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2684                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2685                        self._figi if self._figi else "—",
2686                        instrument["ticker"] if instrument else "—",
2687                        instrument["type"] if instrument else "—",
2688                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2689                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2690                        TKS_OPERATION_STATES[item["state"]],
2691                        TKS_OPERATION_TYPES[item["operationType"]],
2692                    ))
2693
2694            infoText = "".join(info)
2695
2696            if show and not onlyFiles:
2697                if self.moreDebug:
2698                    uLogger.debug("Records about history of a client's operations successfully received")
2699
2700                uLogger.info(infoText)
2701
2702            if self.reportFile and (show or onlyFiles):
2703                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2704                    fH.write(infoText)
2705
2706                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2707
2708                if self.useHTMLReports:
2709                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2710                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2711                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2712
2713                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2714
2715        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False, onlyFiles=False) -> pandas.core.frame.DataFrame:
2717    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2718        """
2719        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2720
2721        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2722        Warning! Broker server used ISO UTC time by default.
2723
2724        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2725        Also, `historyFile` used to update history with `onlyMissing` parameter.
2726
2727        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2728
2729        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2730        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2731        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2732                         `"hour"`, `"day"`. Default: `"hour"`.
2733        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2734                            False by default. Warning! History appends only from last candle to current time
2735                            with always update last candle!
2736        :param csvSep: separator if csv-file is used, `,` by default.
2737        :param show: if `True` then also prints Pandas DataFrame to the console.
2738        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2739        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2740                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2741        """
2742        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2743        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2744        history = None  # empty pandas object for history
2745
2746        if interval not in TKS_CANDLE_INTERVALS.keys():
2747            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2748            raise Exception("Incorrect value")
2749
2750        if not (self._ticker or self._figi):
2751            uLogger.error("Ticker or FIGI must be defined!")
2752            raise Exception("Ticker or FIGI required")
2753
2754        if self._ticker and not self._figi:
2755            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2756            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2757
2758        if self._figi and not self._ticker:
2759            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2760            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2761
2762        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2763        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2764        if interval.lower() != "day":
2765            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2766
2767        delta = dtEnd - dtStart  # current UTC time minus last time in file
2768        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2769
2770        # calculate history length in candles:
2771        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2772        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2773            length += 1  # to avoid fraction time
2774
2775        # calculate data blocks count:
2776        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2777
2778        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2779        if self.moreDebug:
2780            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2781            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2782            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2783            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2784
2785        tempOld = None  # pandas object for old history, if --only-missing key present
2786        lastTime = None  # datetime object of last old candle in file
2787
2788        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2789            if self.moreDebug:
2790                uLogger.debug("--only-missing key present, add only last missing candles...")
2791                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2792
2793            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2794
2795            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2796            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2797            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2798            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2799
2800            # get last datetime object from last string in file or minus 1 delta if file is empty:
2801            if len(tempOld) > 0:
2802                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2803
2804            else:
2805                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2806
2807            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2808
2809        responseJSONs = []  # raw history blocks of data
2810
2811        blockEnd = dtEnd
2812        for item in range(blocks):
2813            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2814            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2815
2816            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2817                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2818            ))
2819
2820            if blockStart == blockEnd:
2821                uLogger.debug("Skipped this zero-length block...")
2822
2823            else:
2824                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2825                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2826                self.body = str({
2827                    "figi": self._figi,
2828                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2829                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2830                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2831                })
2832                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2833
2834                if "code" in responseJSON.keys():
2835                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2836
2837                else:
2838                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2839                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2840
2841                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2842
2843            blockEnd = blockStart
2844
2845        printCount = len(responseJSONs)  # candles to show in console
2846        if responseJSONs:
2847            tempHistory = pd.DataFrame(
2848                data={
2849                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2850                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2851                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2852                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2853                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2854                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2855                    "volume": [int(item["volume"]) for item in responseJSONs],
2856                },
2857                index=range(len(responseJSONs)),
2858                columns=["date", "time", "open", "high", "low", "close", "volume"],
2859            )
2860            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2861            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2862
2863            # append only newest candles to old history if --only-missing key present:
2864            if onlyMissing and tempOld is not None and lastTime is not None:
2865                index = 0  # find start index in tempHistory data:
2866
2867                for i, item in tempHistory.iterrows():
2868                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2869
2870                    if curTime == lastTime:
2871                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2872                        index = i
2873                        printCount = index + 1
2874                        break
2875
2876                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2877
2878            else:
2879                history = tempHistory  # if no `--only-missing` key then load full data from server
2880
2881            if self.moreDebug:
2882                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2883
2884        if history is not None and not history.empty:
2885            if show and not onlyFiles:
2886                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2887                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2888                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2889                ))
2890
2891        else:
2892            uLogger.warning("Received an empty candles history!")
2893
2894        if self.historyFile is not None:
2895            if history is not None and not history.empty:
2896                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2897                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2898
2899            else:
2900                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2901
2902        else:
2903            if self.moreDebug:
2904                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2905
2906        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2908    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2909        """
2910        Load candles history from csv-file and return Pandas DataFrame object.
2911
2912        See also: `History()` and `ShowHistoryChart()` methods.
2913
2914        :param filePath: path to csv-file to open.
2915        """
2916        loadedHistory = None  # init candles data object
2917
2918        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2919
2920        if os.path.exists(filePath):
2921            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2922
2923            tfStr = self.priceModel.FormattedDelta(
2924                self.priceModel.timeframe,
2925                "{days} days {hours}h {minutes}m {seconds}s",
2926            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2927                self.priceModel.timeframe,
2928                "{hours}h {minutes}m {seconds}s",
2929            )
2930
2931            if loadedHistory is not None and not loadedHistory.empty:
2932                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2933                    len(loadedHistory),
2934                    tfStr,
2935                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2936                )
2937
2938            else:
2939                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2940
2941        else:
2942            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2943
2944        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2946    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2947        """
2948        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2949
2950        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2951        Default: `index.html` (both for interact and non-interact candlesticks chart).
2952
2953        See also: `History()` and `LoadHistory()` methods.
2954
2955        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2956        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2957                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2958                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2959                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2960        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2961                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2962        """
2963        if isinstance(candles, str):
2964            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2965            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2966
2967        elif isinstance(candles, pd.DataFrame):
2968            self.priceModel.prices = candles  # set candles chain from variable
2969            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2970
2971            if "datetime" not in candles.columns:
2972                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2973
2974        else:
2975            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2976            raise Exception("Incorrect value")
2977
2978        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2979
2980        if interact:
2981            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2982
2983            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2984
2985        else:
2986            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2987
2988            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2989
2990        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2992    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2993        """
2994        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2995        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2996
2997        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2998
2999        :param operation: string "Buy" or "Sell".
3000        :param lots: volume, integer count of lots >= 1.
3001        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
3002        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
3003        :param expDate: string "Undefined" by default or local date in future,
3004                        it is a string with format `%Y-%m-%d %H:%M:%S`.
3005        :return: JSON with response from broker server.
3006        """
3007        if self.accountId is None or not self.accountId:
3008            uLogger.error("Variable `accountId` must be defined for using this method!")
3009            raise Exception("Account ID required")
3010
3011        if operation is None or not operation or operation not in ("Buy", "Sell"):
3012            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3013            raise Exception("Incorrect value")
3014
3015        if lots is None or lots < 1:
3016            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
3017            lots = 1
3018
3019        if tp is None or tp < 0:
3020            tp = 0
3021
3022        if sl is None or sl < 0:
3023            sl = 0
3024
3025        if expDate is None or not expDate:
3026            expDate = "Undefined"
3027
3028        if not (self._ticker or self._figi):
3029            uLogger.error("Ticker or FIGI must be defined!")
3030            raise Exception("Ticker or FIGI required")
3031
3032        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3033        self._ticker = instrument["ticker"]
3034        self._figi = instrument["figi"]
3035
3036        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3037
3038        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3039        self.body = str({
3040            "figi": self._figi,
3041            "quantity": str(lots),
3042            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3043            "accountId": str(self.accountId),
3044            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3045        })
3046        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3047
3048        if "orderId" in response.keys():
3049            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3050                operation, response["orderId"],
3051                self._ticker, self._figi, lots,
3052                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3053                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3054                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3055            ))
3056
3057            if tp > 0:
3058                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3059
3060            if sl > 0:
3061                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3062
3063        else:
3064            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3065
3066        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3068    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3069        """
3070        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3071        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3072
3073        See also: `Order()` and `Trade()` docstrings.
3074
3075        :param lots: volume, integer count of lots >= 1.
3076        :param tp: float > 0, take profit price of stop-order.
3077        :param sl: float > 0, stop loss price of stop-order.
3078        :param expDate: it's a local date in future.
3079                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3080        :return: JSON with response from broker server.
3081        """
3082        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3084    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3085        """
3086        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3087        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3088
3089        See also: `Order()` and `Trade()` docstrings.
3090
3091        :param lots: volume, integer count of lots >= 1.
3092        :param tp: float > 0, take profit price of stop-order.
3093        :param sl: float > 0, stop loss price of stop-order.
3094        :param expDate: it's a local date in the future.
3095                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3096        :return: JSON with response from broker server.
3097        """
3098        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3100    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3101        """
3102        Close position of given instruments.
3103
3104        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3105        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3106                         This avoids unnecessary downloading data from the server.
3107        """
3108        if instruments is None or not instruments:
3109            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3110            raise Exception("Ticker or FIGI required")
3111
3112        if isinstance(instruments, str):
3113            instruments = [instruments]
3114
3115        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3116        if uniqueInstruments:
3117            if portfolio is None or not portfolio:
3118                portfolio = self.Overview(show=False)
3119
3120            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3121            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3122
3123            for self._figi in uniqueInstruments:
3124                if self._figi not in allOpened:
3125                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3126                    continue
3127
3128                # search open trade info about instrument by ticker:
3129                instrument = {}
3130                for iType in TKS_INSTRUMENTS:
3131                    if instrument:
3132                        break
3133
3134                    for item in portfolio["stat"][iType]:
3135                        if item["figi"] == self._figi:
3136                            instrument = item
3137                            break
3138
3139                if instrument:
3140                    self._ticker = instrument["ticker"]
3141                    self._figi = instrument["figi"]
3142
3143                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3144                        self._ticker,
3145                        self._figi,
3146                        int(instrument["volume"]),
3147                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3148                    ))
3149
3150                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3151
3152                    if tradeLots > 0:
3153                        if instrument["blocked"] > 0:
3154                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3155                                instrument["blocked"],
3156                                self._ticker,
3157                                tradeLots,
3158                            ))
3159
3160                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3161                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3162
3163                    else:
3164                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3166    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3167        """
3168        Close all positions of given instruments with defined type.
3169
3170        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3171        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3172                         This avoids unnecessary downloading data from the server.
3173        """
3174        if iType not in TKS_INSTRUMENTS:
3175            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3176
3177        else:
3178            if portfolio is None or not portfolio:
3179                portfolio = self.Overview(show=False)
3180
3181            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3182            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3183
3184            if tickers and portfolio:
3185                self.CloseTrades(tickers, portfolio)
3186
3187            else:
3188                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3190    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3191        """
3192        Universal method to create market or limit orders with all available parameters for current `accountId`.
3193        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3194
3195        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3196        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3197
3198        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3199        then broker immediately open market order as you can do simple --buy or --sell operations!
3200
3201        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3202        When current price will go up or down to target price value then broker opens a limit order.
3203        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3204
3205        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3206
3207        :param operation: string "Buy" or "Sell".
3208        :param orderType: string "Limit" or "Stop".
3209        :param lots: volume, integer count of lots >= 1.
3210        :param targetPrice: target price > 0. This is open trade price for limit order.
3211        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3212                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3213        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3214                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3215                         Stop loss order always executed by market price.
3216        :param expDate: string "Undefined" by default or local date in future.
3217                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3218                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3219                        A limit order has no expiration date, it lasts until the end of the trading day.
3220        :return: JSON with response from broker server.
3221        """
3222        if self.accountId is None or not self.accountId:
3223            uLogger.error("Variable `accountId` must be defined for using this method!")
3224            raise Exception("Account ID required")
3225
3226        if operation is None or not operation or operation not in ("Buy", "Sell"):
3227            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3228            raise Exception("Incorrect value")
3229
3230        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3231            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3232            raise Exception("Incorrect value")
3233
3234        if lots is None or lots < 1:
3235            uLogger.error("You must define trade volume > 0: integer count of lots!")
3236            raise Exception("Incorrect value")
3237
3238        if targetPrice is None or targetPrice <= 0:
3239            uLogger.error("Target price for limit-order must be greater than 0!")
3240            raise Exception("Incorrect value")
3241
3242        if limitPrice is None or limitPrice <= 0:
3243            limitPrice = targetPrice
3244
3245        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3246            stopType = "Limit"
3247
3248        if expDate is None or not expDate:
3249            expDate = "Undefined"
3250
3251        if not (self._ticker or self._figi):
3252            uLogger.error("Tocker or FIGI must be defined!")
3253            raise Exception("Ticker or FIGI required")
3254
3255        response = {}
3256        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3257        self._ticker = instrument["ticker"]
3258        self._figi = instrument["figi"]
3259
3260        if orderType == "Limit":
3261            uLogger.debug(
3262                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3263                    self._ticker, self._figi,
3264                    operation, lots, targetPrice, instrument["currency"],
3265                ))
3266
3267            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3268            self.body = str({
3269                "figi": self._figi,
3270                "quantity": str(lots),
3271                "price": FloatToNano(targetPrice),
3272                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3273                "accountId": str(self.accountId),
3274                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3275            })
3276            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3277
3278            if "orderId" in response.keys():
3279                uLogger.info(
3280                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3281                        response["orderId"], self._ticker, self._figi, operation, lots,
3282                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3283                    ))
3284
3285                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3286                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3287                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3288                            targetPrice, instrument["currency"],
3289                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3290                        ))
3291
3292                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3293                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3294                            targetPrice, instrument["currency"],
3295                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3296                        ))
3297
3298            else:
3299                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3300
3301        if orderType == "Stop":
3302            uLogger.debug(
3303                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3304                    self._ticker, self._figi,
3305                    operation, lots,
3306                    targetPrice, instrument["currency"],
3307                    limitPrice, instrument["currency"],
3308                    stopType, expDate,
3309                ))
3310
3311            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3312            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3313            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3314
3315            body = {
3316                "figi": self._figi,
3317                "quantity": str(lots),
3318                "price": FloatToNano(limitPrice),
3319                "stopPrice": FloatToNano(targetPrice),
3320                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3321                "accountId": str(self.accountId),
3322                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3323                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3324            }
3325
3326            if expDateUTC:
3327                body["expireDate"] = expDateUTC
3328
3329            self.body = str(body)
3330            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3331
3332            if "stopOrderId" in response.keys():
3333                uLogger.info(
3334                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3335                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3336                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3337                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3338                        TKS_STOP_ORDER_TYPES[stopOrderType],
3339                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3340                    ))
3341
3342                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3343                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3344                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3345                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3346                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3347                        ))
3348
3349                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3350                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3351                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3352                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3353                        ))
3354
3355            else:
3356                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3357
3358        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3360    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3361        """
3362        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3363        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3364        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3365        See also: `Order()` docstring.
3366
3367        :param lots: volume, integer count of lots >= 1.
3368        :param targetPrice: target price > 0. This is open trade price for limit order.
3369        :return: JSON with response from broker server.
3370        """
3371        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3373    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3374        """
3375        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3376        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3377        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3378        target price value then broker opens a limit order. See also: `Order()` docstring.
3379
3380        :param lots: volume, integer count of lots >= 1.
3381        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3382        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3383                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3384        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3385                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3386        :param expDate: string "Undefined" by default or local date in future.
3387                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3388                        This date is converting to UTC format for server.
3389        :return: JSON with response from broker server.
3390        """
3391        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3393    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3394        """
3395        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3396        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3397        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3398        See also: `Order()` docstring.
3399
3400        :param lots: volume, integer count of lots >= 1.
3401        :param targetPrice: target price > 0. This is open trade price for limit order.
3402        :return: JSON with response from broker server.
3403        """
3404        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3406    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3407        """
3408        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3409        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3410        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3411        target price value then broker opens a limit order. See also: `Order()` docstring.
3412
3413        :param lots: volume, integer count of lots >= 1.
3414        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3415        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3416                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3417        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3418                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3419        :param expDate: string "Undefined" by default or local date in future.
3420                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3421                        This date is converting to UTC format for server.
3422        :return: JSON with response from broker server.
3423        """
3424        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3426    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3427        """
3428        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3429
3430        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3431        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3432                             This avoids unnecessary downloading data from the server.
3433        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3434        """
3435        if self.accountId is None or not self.accountId:
3436            uLogger.error("Variable `accountId` must be defined for using this method!")
3437            raise Exception("Account ID required")
3438
3439        if orderIDs:
3440            if allOrdersIDs is None:
3441                rawOrders = self.RequestPendingOrders()
3442                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3443
3444            if allStopOrdersIDs is None:
3445                rawStopOrders = self.RequestStopOrders()
3446                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3447
3448            for orderID in orderIDs:
3449                idInPendingOrders = orderID in allOrdersIDs
3450                idInStopOrders = orderID in allStopOrdersIDs
3451
3452                if not (idInPendingOrders or idInStopOrders):
3453                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3454                    continue
3455
3456                else:
3457                    if idInPendingOrders:
3458                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3459
3460                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3461                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3462                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3463                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3464
3465                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3466                            if self.moreDebug:
3467                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3468
3469                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3470
3471                        else:
3472                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3473
3474                    elif idInStopOrders:
3475                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3476
3477                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3478                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3479                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3480                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3481
3482                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3483                            if self.moreDebug:
3484                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3485
3486                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3487
3488                        else:
3489                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3490
3491                    else:
3492                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3494    def CloseAllOrders(self) -> None:
3495        """
3496        Gets a list of open pending and stop orders and cancel it all.
3497        """
3498        rawOrders = self.RequestPendingOrders()
3499        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3500        lenOrders = len(allOrdersIDs)
3501
3502        rawStopOrders = self.RequestStopOrders()
3503        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3504        lenSOrders = len(allStopOrdersIDs)
3505
3506        if lenOrders > 0 or lenSOrders > 0:
3507            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3508
3509            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3510
3511        else:
3512            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3514    def CloseAll(self, *args) -> None:
3515        """
3516        Close all available (not blocked) opened trades and orders.
3517
3518        Also, you can select one or more keywords case-insensitive:
3519        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3520
3521        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3522        """
3523        overview = self.Overview(show=False)  # get all open trades info
3524
3525        if len(args) == 0:
3526            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3527            self.CloseAllOrders()  # close all pending and stop orders
3528
3529            for iType in TKS_INSTRUMENTS:
3530                if iType != "Currencies":
3531                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3532
3533        else:
3534            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3535            lowerArgs = [x.lower() for x in args]
3536
3537            if "orders" in lowerArgs:
3538                self.CloseAllOrders()  # close all pending and stop orders
3539
3540            for iType in TKS_INSTRUMENTS:
3541                if iType.lower() in lowerArgs and iType != "Currencies":
3542                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3544    def CloseAllByTicker(self, instrument: str) -> None:
3545        """
3546        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3547
3548        This method searches opened trade and orders of instrument throw all portfolio and then use
3549        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3550
3551        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3552
3553        :param instrument: string with ticker.
3554        """
3555        if instrument is None or not instrument:
3556            uLogger.error("Ticker name must be defined for using this method!")
3557            raise Exception("Ticker required")
3558
3559        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3560
3561        self._ticker = instrument  # try to set instrument as ticker
3562        self._figi = ""
3563
3564        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3565        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3566
3567        if limitAll and self.IsInLimitOrders(portfolio=overview):
3568            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3569            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3570
3571        if stopAll and self.IsInStopOrders(portfolio=overview):
3572            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3573            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3574
3575        if self.IsInPortfolio(portfolio=overview):
3576            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3577            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3579    def CloseAllByFIGI(self, instrument: str) -> None:
3580        """
3581        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3582
3583        This method searches opened trade and orders of instrument throw all portfolio and then use
3584        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3585
3586        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3587
3588        :param instrument: string with FIGI id.
3589        """
3590        if instrument is None or not instrument:
3591            uLogger.error("FIGI id must be defined for using this method!")
3592            raise Exception("FIGI required")
3593
3594        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3595
3596        self._ticker = ""
3597        self._figi = instrument  # try to set instrument as FIGI id
3598
3599        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3600        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3601
3602        if limitAll and self.IsInLimitOrders(portfolio=overview):
3603            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3604            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3605
3606        if stopAll and self.IsInStopOrders(portfolio=overview):
3607            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3608            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3609
3610        if self.IsInPortfolio(portfolio=overview):
3611            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3612            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters):
3614    @staticmethod
3615    def ParseOrderParameters(operation, **inputParameters):
3616        """
3617        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3618
3619        :param operation: string "Buy" or "Sell".
3620        :param inputParameters: this is dict of strings that looks like this
3621               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3622               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3623               "prices" key: one or more prices to open limit-orders
3624               Counts of values in lots and prices lists must be equals!
3625        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3626        """
3627        # TODO: update order grid work with api v2
3628        pass
3629        # uLogger.debug("Input parameters: {}".format(inputParameters))
3630        #
3631        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3632        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3633        #     raise Exception("Incorrect value")
3634        #
3635        # if "l" in inputParameters.keys():
3636        #     inputParameters["lots"] = inputParameters.pop("l")
3637        #
3638        # if "p" in inputParameters.keys():
3639        #     inputParameters["prices"] = inputParameters.pop("p")
3640        #
3641        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3642        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3643        #     raise Exception("Incorrect value")
3644        #
3645        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3646        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3647        #
3648        # if len(lots) != len(prices):
3649        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3650        #     raise Exception("Incorrect value")
3651        #
3652        # uLogger.debug("Extracted parameters for orders:")
3653        # uLogger.debug("lots = {}".format(lots))
3654        # uLogger.debug("prices = {}".format(prices))
3655        #
3656        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3657        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3658        # uLogger.debug("Order parameters: {}".format(result))
3659        #
3660        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3662    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3663        """
3664        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3665
3666        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3667        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3668        """
3669        result = False
3670        msg = "Instrument not defined!"
3671
3672        if portfolio is None or not portfolio:
3673            portfolio = self.Overview(show=False)
3674
3675        if self._ticker:
3676            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3677            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3678
3679            for iType in TKS_INSTRUMENTS:
3680                for instrument in portfolio["stat"][iType]:
3681                    if instrument["ticker"] == self._ticker:
3682                        result = True
3683                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3684                        break
3685
3686        elif self._figi:
3687            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3688            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3689
3690            for iType in TKS_INSTRUMENTS:
3691                for instrument in portfolio["stat"][iType]:
3692                    if instrument["figi"] == self._figi:
3693                        result = True
3694                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3695                        break
3696
3697        else:
3698            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3699
3700        uLogger.debug(msg)
3701
3702        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3704    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3705        """
3706        Returns instrument from the user's portfolio if it presents there.
3707        Instrument must be defined by `ticker` (highly priority) or `figi`.
3708
3709        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3710        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3711        """
3712        result = None
3713        msg = "Instrument not defined!"
3714
3715        if portfolio is None or not portfolio:
3716            portfolio = self.Overview(show=False)
3717
3718        if self._ticker:
3719            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3720            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3721
3722            for iType in TKS_INSTRUMENTS:
3723                for instrument in portfolio["stat"][iType]:
3724                    if instrument["ticker"] == self._ticker:
3725                        result = instrument
3726                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3727                        break
3728
3729        elif self._figi:
3730            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3731            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3732
3733            for iType in TKS_INSTRUMENTS:
3734                for instrument in portfolio["stat"][iType]:
3735                    if instrument["figi"] == self._figi:
3736                        result = instrument
3737                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3738                        break
3739
3740        else:
3741            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3742
3743        uLogger.debug(msg)
3744
3745        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3747    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3748        """
3749        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3750
3751        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3752
3753        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3754        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3755        """
3756        result = False
3757        msg = "Instrument not defined!"
3758
3759        if portfolio is None or not portfolio:
3760            portfolio = self.Overview(show=False)
3761
3762        if self._ticker:
3763            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3764            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3765
3766            for instrument in portfolio["stat"]["orders"]:
3767                if instrument["ticker"] == self._ticker:
3768                    result = True
3769                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3770                    break
3771
3772        elif self._figi:
3773            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3774            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3775
3776            for instrument in portfolio["stat"]["orders"]:
3777                if instrument["figi"] == self._figi:
3778                    result = True
3779                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3780                    break
3781
3782        else:
3783            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3784
3785        uLogger.debug(msg)
3786
3787        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3789    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3790        """
3791        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3792        Instrument must be defined by `ticker` (highly priority) or `figi`.
3793
3794        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3795
3796        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3797        :return: list with `orderID`s of limit orders.
3798        """
3799        result = []
3800        msg = "Instrument not defined!"
3801
3802        if portfolio is None or not portfolio:
3803            portfolio = self.Overview(show=False)
3804
3805        if self._ticker:
3806            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3807            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3808
3809            for instrument in portfolio["stat"]["orders"]:
3810                if instrument["ticker"] == self._ticker:
3811                    result.append(instrument["orderID"])
3812
3813            if result:
3814                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3815
3816        elif self._figi:
3817            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3818            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3819
3820            for instrument in portfolio["stat"]["orders"]:
3821                if instrument["figi"] == self._figi:
3822                    result.append(instrument["orderID"])
3823
3824            if result:
3825                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3826
3827        else:
3828            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3829
3830        uLogger.debug(msg)
3831
3832        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3834    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3835        """
3836        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3837
3838        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3839
3840        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3841        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3842        """
3843        result = False
3844        msg = "Instrument not defined!"
3845
3846        if portfolio is None or not portfolio:
3847            portfolio = self.Overview(show=False)
3848
3849        if self._ticker:
3850            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3851            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3852
3853            for instrument in portfolio["stat"]["stopOrders"]:
3854                if instrument["ticker"] == self._ticker:
3855                    result = True
3856                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3857                    break
3858
3859        elif self._figi:
3860            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3861            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3862
3863            for instrument in portfolio["stat"]["stopOrders"]:
3864                if instrument["figi"] == self._figi:
3865                    result = True
3866                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3867                    break
3868
3869        else:
3870            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3871
3872        uLogger.debug(msg)
3873
3874        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3876    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3877        """
3878        Returns list with all `orderID`s of opened stop orders for the instrument.
3879        Instrument must be defined by `ticker` (highly priority) or `figi`.
3880
3881        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3882
3883        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3884        :return: list with `orderID`s of stop orders.
3885        """
3886        result = []
3887        msg = "Instrument not defined!"
3888
3889        if portfolio is None or not portfolio:
3890            portfolio = self.Overview(show=False)
3891
3892        if self._ticker:
3893            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3894            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3895
3896            for instrument in portfolio["stat"]["stopOrders"]:
3897                if instrument["ticker"] == self._ticker:
3898                    result.append(instrument["orderID"])
3899
3900            if result:
3901                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3902
3903        elif self._figi:
3904            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3905            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3906
3907            for instrument in portfolio["stat"]["stopOrders"]:
3908                if instrument["figi"] == self._figi:
3909                    result.append(instrument["orderID"])
3910
3911            if result:
3912                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3913
3914        else:
3915            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3916
3917        uLogger.debug(msg)
3918
3919        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3921    def RequestLimits(self) -> dict:
3922        """
3923        Method for obtaining the available funds for withdrawal for current `accountId`.
3924
3925        See also:
3926        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3927        - `OverviewLimits()` method
3928
3929        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3930                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3931                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3932                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3933        """
3934        if self.accountId is None or not self.accountId:
3935            uLogger.error("Variable `accountId` must be defined for using this method!")
3936            raise Exception("Account ID required")
3937
3938        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3939
3940        self.body = str({"accountId": self.accountId})
3941        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3942        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3943
3944        if self.moreDebug:
3945            uLogger.debug("Records about available funds for withdrawal successfully received")
3946
3947        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3949    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3950        """
3951        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3952
3953        See also: `RequestLimits()`.
3954
3955        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3956        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3957        :return: dict with raw parsed data from server and some calculated statistics about it.
3958        """
3959        if self.accountId is None or not self.accountId:
3960            uLogger.error("Variable `accountId` must be defined for using this method!")
3961            raise Exception("Account ID required")
3962
3963        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3964
3965        view = {
3966            "rawLimits": rawLimits,
3967            "limits": {  # parsed data for every currency:
3968                "money": {  # this is an array of portfolio currency positions
3969                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3970                },
3971                "blocked": {  # this is an array of blocked currency
3972                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3973                },
3974                "blockedGuarantee": {  # this is locked money under collateral for futures
3975                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3976                },
3977            },
3978        }
3979
3980        # --- Prepare text table with limits in human-readable format:
3981        if show or onlyFiles:
3982            info = [
3983                "# Withdrawal limits\n\n",
3984                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3985                "* **Account ID:** [{}]\n".format(self.accountId),
3986            ]
3987
3988            if view["limits"]["money"]:
3989                info.extend([
3990                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3991                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3992                ])
3993
3994            else:
3995                info.append("\nNo withdrawal limits\n")
3996
3997            for curr in view["limits"]["money"].keys():
3998                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3999                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
4000                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
4001
4002                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
4003                    "[{}]".format(curr),
4004                    "{:.2f}".format(view["limits"]["money"][curr]),
4005                    "{:.2f}".format(availableMoney),
4006                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
4007                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
4008                )
4009
4010                if curr == "rub":
4011                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
4012
4013                else:
4014                    info.append(infoStr)
4015
4016            infoText = "".join(info)
4017
4018            if show and not onlyFiles:
4019                uLogger.info(infoText)
4020
4021            if self.withdrawalLimitsFile and (show or onlyFiles):
4022                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
4023                    fH.write(infoText)
4024
4025                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
4026
4027                if self.useHTMLReports:
4028                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
4029                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4030                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4031
4032                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4033
4034        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
4036    def RequestAccounts(self) -> dict:
4037        """
4038        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4039
4040        See also:
4041        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4042        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4043        - `OverviewUserInfo()` method
4044
4045        :return: dict with raw data from server that contains accounts info. Example of dict:
4046                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4047                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4048                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4049                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4050        """
4051        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4052
4053        self.body = str({})
4054        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4055        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4056
4057        if self.moreDebug:
4058            uLogger.debug("Records about available accounts successfully received")
4059
4060        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
4062    def RequestUserInfo(self) -> dict:
4063        """
4064        Method for requesting common user's information.
4065
4066        See also:
4067        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4068        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4069        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4070        - `OverviewUserInfo()` method
4071
4072        :return: dict with raw data from server that contains user's information. Example of dict:
4073                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4074                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4075        """
4076        uLogger.debug("Requesting common user's information. Wait, please...")
4077
4078        self.body = str({})
4079        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4080        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4081
4082        if self.moreDebug:
4083            uLogger.debug("Records about current user successfully received")
4084
4085        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
4087    def RequestMarginStatus(self, accountId: str = None) -> dict:
4088        """
4089        Method for requesting margin calculation for defined account ID.
4090
4091        See also:
4092        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4093        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4094        - `OverviewUserInfo()` method
4095
4096        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4097        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4098                 Example of responses:
4099                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4100                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4101                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4102                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4103                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4104                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4105        """
4106        if accountId is None or not accountId:
4107            if self.accountId is None or not self.accountId:
4108                uLogger.error("Variable `accountId` must be defined for using this method!")
4109                raise Exception("Account ID required")
4110
4111            else:
4112                accountId = self.accountId  # use `self.accountId` (main ID) by default
4113
4114        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4115
4116        self.body = str({"accountId": accountId})
4117        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4118        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4119
4120        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4121            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4122            rawMargin = {}
4123
4124        else:
4125            if self.moreDebug:
4126                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4127
4128        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
4130    def RequestTariffLimits(self) -> dict:
4131        """
4132        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4133
4134        See also:
4135        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4136        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4137        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4138        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4139        - `OverviewUserInfo()` method
4140
4141        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4142                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4143                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4144        """
4145        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4146
4147        self.body = str({})
4148        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4149        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4150
4151        if self.moreDebug:
4152            uLogger.debug("Records with limits of current tariff successfully received")
4153
4154        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
4156    def RequestBondCoupons(self, iJSON: dict) -> dict:
4157        """
4158        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4159        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4160        All dates are in UTC timezone.
4161
4162        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4163        Documentation:
4164        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4165        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4166
4167        See also: `ExtendBondsData()`.
4168
4169        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4170                      If raw iJSON is not data of bond then server returns an error [400] with message:
4171                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4172        :return: dictionary with bond payment calendar. Response example
4173                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4174                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4175                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4176                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4177        """
4178        if iJSON["figi"] is None or not iJSON["figi"]:
4179            uLogger.error("FIGI must be defined for using this method!")
4180            raise Exception("FIGI required")
4181
4182        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4183        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4184
4185        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4186            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4187            self._figi,
4188            startDate,
4189            endDate,
4190        ))
4191
4192        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4193        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4194        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4195
4196        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4197            uLogger.warning("Instrument type is not bond!")
4198
4199        else:
4200            if self.moreDebug:
4201                uLogger.debug("Records about bond payment calendar successfully received")
4202
4203        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self._ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4205    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4206        """
4207        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4208        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4209        coupon yields, current yields and some statistics etc.
4210
4211        WARNING! This is too long operation if a lot of bonds requested from broker server.
4212
4213        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4214
4215        :param instruments: list of strings with tickers or FIGIs.
4216        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4217                     for further used by data scientists or stock analytics.
4218        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4219                 In XLSX-file and Pandas DataFrame fields mean:
4220                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4221                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4222        """
4223        if instruments is None or not instruments:
4224            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4225            raise Exception("Ticker or FIGI required")
4226
4227        if isinstance(instruments, str):
4228            instruments = [instruments]
4229
4230        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4231
4232        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4233
4234        iCount = len(uniqueInstruments)
4235        tooLong = iCount >= 20
4236        if tooLong:
4237            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4238
4239        bonds = None
4240        for i, self._figi in enumerate(uniqueInstruments):
4241            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4242
4243            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4244                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4245                rawBond = self.SearchByFIGI(requestPrice=True)
4246
4247                # Widen raw data with UTC current time (iData["actualDateTime"]):
4248                actualDate = datetime.now(tzutc())
4249                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4250
4251                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4252                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4253
4254                # Replace some values with human-readable:
4255                iData["nominalCurrency"] = iData["nominal"]["currency"]
4256                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4257                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4258                iData["aciCurrency"] = iData["aciValue"]["currency"]
4259                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4260                iData["issueSize"] = int(iData["issueSize"])
4261                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4262                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4263                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4264                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4265                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4266                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4267                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4268                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4269                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4270                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4271
4272                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4273                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4274                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4275                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4276                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4277                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4278                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4279                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4280                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4281                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4282                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4283
4284                # Widen raw data with calendar data from `rawCalendar` values:
4285                calendarData = []
4286                if "events" in iData["rawCalendar"].keys():
4287                    for item in iData["rawCalendar"]["events"]:
4288                        calendarData.append({
4289                            "couponDate": item["couponDate"],
4290                            "couponNumber": int(item["couponNumber"]),
4291                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4292                            "payCurrency": item["payOneBond"]["currency"],
4293                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4294                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4295                            "couponStartDate": item["couponStartDate"],
4296                            "couponEndDate": item["couponEndDate"],
4297                            "couponPeriod": item["couponPeriod"],
4298                        })
4299
4300                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4301                    if "maturityDate" not in iData.keys():
4302                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4303
4304                # Widen raw data with Coupon Rate.
4305                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4306                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4307                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4308                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4309
4310                # Widen raw data with Yield to Maturity (YTM) on current date.
4311                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4312                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4313                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4314                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4315                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4316                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4317
4318                iData["calendar"] = calendarData  # adds calendar at the end
4319
4320                # Remove not used data:
4321                iData.pop("uid")
4322                iData.pop("positionUid")
4323                iData.pop("currentPrice")
4324                iData.pop("rawCalendar")
4325
4326                colNames = list(iData.keys())
4327                if bonds is None:
4328                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4329
4330                else:
4331                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4332
4333            else:
4334                uLogger.warning("Instrument is not a bond!")
4335
4336            processed = round(100 * (i + 1) / iCount, 1)
4337            if tooLong and processed % 5 == 0:
4338                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4339
4340            else:
4341                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4342
4343        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4344
4345        # Saving bonds from Pandas DataFrame to XLSX sheet:
4346        if xlsx and self.bondsXLSXFile:
4347            with pd.ExcelWriter(
4348                    path=self.bondsXLSXFile,
4349                    date_format=TKS_DATE_FORMAT,
4350                    datetime_format=TKS_DATE_TIME_FORMAT,
4351                    mode="w",
4352            ) as writer:
4353                bonds.to_excel(
4354                    writer,
4355                    sheet_name="Extended bonds data",
4356                    index=True,
4357                    encoding="UTF-8",
4358                    freeze_panes=(1, 1),
4359                )  # saving as XLSX-file with freeze first row and column as headers
4360
4361            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4362
4363        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4365    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4366        """
4367        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4368
4369        WARNING! This is too long operation if a lot of bonds requested from broker server.
4370
4371        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4372
4373        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4374                        extended information about bonds: main info, current prices, bond payment calendar,
4375                        coupon yields, current yields and some statistics etc.
4376                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4377        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4378                     for further used by data scientists or stock analytics.
4379        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4380        """
4381        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4382            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4383
4384        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4385
4386        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4387        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4388        calendar = None
4389        for bond in extBonds.iterrows():
4390            for item in bond[1]["calendar"]:
4391                cData = {
4392                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4393                    "couponDate": item["couponDate"],
4394                    "figi": bond[1]["figi"],
4395                    "ticker": bond[1]["ticker"],
4396                    "name": bond[1]["name"],
4397                    "couponNumber": item["couponNumber"],
4398                    "payOneBond": item["payOneBond"],
4399                    "payCurrency": item["payCurrency"],
4400                    "couponType": item["couponType"],
4401                    "couponPeriod": item["couponPeriod"],
4402                    "fixDate": item["fixDate"],
4403                    "couponStartDate": item["couponStartDate"],
4404                    "couponEndDate": item["couponEndDate"],
4405                }
4406
4407                if calendar is None:
4408                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4409
4410                else:
4411                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4412
4413        if calendar is not None:
4414            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4415
4416            # Saving calendar from Pandas DataFrame to XLSX sheet:
4417            if xlsx:
4418                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4419
4420                with pd.ExcelWriter(
4421                        path=xlsxCalendarFile,
4422                        date_format=TKS_DATE_FORMAT,
4423                        datetime_format=TKS_DATE_TIME_FORMAT,
4424                        mode="w",
4425                ) as writer:
4426                    humanReadable = calendar.copy(deep=True)
4427                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4428                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4429                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4430                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4431                    humanReadable.columns = colNames  # human-readable column names
4432
4433                    humanReadable.to_excel(
4434                        writer,
4435                        sheet_name="Bond payments calendar",
4436                        index=False,
4437                        encoding="UTF-8",
4438                        freeze_panes=(1, 2),
4439                    )  # saving as XLSX-file with freeze first row and column as headers
4440
4441                    del humanReadable  # release df in memory
4442
4443                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4444
4445        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, show: bool = True, onlyFiles=False) -> str:
4447    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4448        """
4449        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4450        Also, creates Markdown file with calendar data, `calendar.md` by default.
4451
4452        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4453
4454        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4455                        extended information about bonds: main info, current prices, bond payment calendar,
4456                        coupon yields, current yields and some statistics etc.
4457                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4458        :param show: if `True` then also printing bonds payment calendar to the console,
4459                     otherwise save to file `calendarFile` only. `False` by default.
4460        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4461        :return: multilines text in Markdown format with bonds payment calendar as a table.
4462        """
4463        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4464            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4465
4466        infoText = "# Bond payments calendar\n\n"
4467
4468        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4469
4470        if not (calendar is None or calendar.empty):
4471            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4472
4473            info = [
4474                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4475                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4476                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4477            ]
4478
4479            newMonth = False
4480            notOneBond = calendar["figi"].nunique() > 1
4481            for i, bond in enumerate(calendar.iterrows()):
4482                if newMonth and notOneBond:
4483                    info.append(splitLine)
4484
4485                info.append(
4486                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4487                        "  √" if bond[1]["paid"] else "  —",
4488                        bond[1]["couponDate"].split("T")[0],
4489                        bond[1]["figi"],
4490                        bond[1]["ticker"],
4491                        bond[1]["couponNumber"],
4492                        "{} {}".format(
4493                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4494                            bond[1]["payCurrency"],
4495                        ),
4496                        bond[1]["couponType"],
4497                        bond[1]["couponPeriod"],
4498                        bond[1]["fixDate"].split("T")[0],
4499                    )
4500                )
4501
4502                if i < len(calendar.values) - 1:
4503                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4504                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4505                    newMonth = False if curDate.month == nextDate.month else True
4506
4507                else:
4508                    newMonth = False
4509
4510            infoText += "".join(info)
4511
4512            if show and not onlyFiles:
4513                uLogger.info("{}".format(infoText))
4514
4515            if self.calendarFile is not None and (show or onlyFiles):
4516                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4517                    fH.write(infoText)
4518
4519                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4520
4521                if self.useHTMLReports:
4522                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4523                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4524                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4525
4526                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4527
4528        else:
4529            infoText += "No data\n"
4530
4531        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4533    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4534        """
4535        Method for parsing and show simple table with all available user accounts.
4536
4537        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4538
4539        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4540        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4541        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4542                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4543                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4544                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4545                                                        "closed": "—", "access": "Full access" }, ...}}`
4546        """
4547        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4548
4549        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4550        accounts = {
4551            item["id"]: {
4552                "type": TKS_ACCOUNT_TYPES[item["type"]],
4553                "name": item["name"],
4554                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4555                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4556                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4557                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4558            } for item in rawAccounts["accounts"]
4559        }
4560
4561        # Raw and parsed data with some fields replaced in "stat" section:
4562        view = {
4563            "rawAccounts": rawAccounts,
4564            "stat": accounts,
4565        }
4566
4567        # --- Prepare simple text table with only accounts data in human-readable format:
4568        if show or onlyFiles:
4569            info = [
4570                "# User accounts\n\n",
4571                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4572                "| Account ID   | Type                      | Status                    | Name                           |\n",
4573                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4574            ]
4575
4576            for account in view["stat"].keys():
4577                info.extend([
4578                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4579                        account,
4580                        view["stat"][account]["type"],
4581                        view["stat"][account]["status"],
4582                        view["stat"][account]["name"],
4583                    )
4584                ])
4585
4586            infoText = "".join(info)
4587
4588            if show and not onlyFiles:
4589                uLogger.info(infoText)
4590
4591            if self.userAccountsFile and (show or onlyFiles):
4592                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4593                    fH.write(infoText)
4594
4595                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4596
4597                if self.useHTMLReports:
4598                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4599                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4600                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4601
4602                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4603
4604        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4606    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4607        """
4608        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4609
4610        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4611
4612        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4613        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4614        :return: dict with raw parsed data from server and some calculated statistics about it.
4615        """
4616        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4617        tmpTicker = self._ticker
4618        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4619        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4620        self._ticker = tmpTicker
4621
4622        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4623        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4624        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4625        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4626        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4627        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4628
4629        # This is dict with parsed common user data:
4630        userInfo = {
4631            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4632            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4633            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4634            "tariff": rawUserInfo["tariff"],
4635        }
4636
4637        # This is an array of dict with parsed margin statuses for every account IDs:
4638        margins = {}
4639        for accountId in accounts.keys():
4640            if rawMargins[accountId]:
4641                margins[accountId] = {
4642                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4643                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4644                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4645                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4646                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4647                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4648                    "missing": missing["volume"],
4649                }
4650
4651            else:
4652                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4653
4654        unary = {}  # unary-connection limits
4655        for item in rawTariffLimits["unaryLimits"]:
4656            if item["limitPerMinute"] in unary.keys():
4657                unary[item["limitPerMinute"]].extend(item["methods"])
4658
4659            else:
4660                unary[item["limitPerMinute"]] = item["methods"]
4661
4662        stream = {}  # stream-connection limits
4663        for item in rawTariffLimits["streamLimits"]:
4664            if item["limit"] in stream.keys():
4665                stream[item["limit"]].extend(item["streams"])
4666
4667            else:
4668                stream[item["limit"]] = item["streams"]
4669
4670        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4671        limits = {
4672            "unary": unary,
4673            "stream": stream,
4674        }
4675
4676        # Raw and parsed data as an output result:
4677        view = {
4678            "rawUserInfo": rawUserInfo,
4679            "rawAccounts": rawAccounts,
4680            "rawMargins": rawMargins,
4681            "rawTariffLimits": rawTariffLimits,
4682            "stat": {
4683                "overview": overview,
4684                "userInfo": userInfo,
4685                "accounts": accounts,
4686                "margins": margins,
4687                "limits": limits,
4688            },
4689        }
4690
4691        # --- Prepare text table with user information in human-readable format:
4692        if show or onlyFiles:
4693            info = [
4694                "# Full user information\n\n",
4695                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4696                "## Common information\n\n",
4697                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4698                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4699                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4700                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4701                "\n## User accounts\n\n",
4702            ]
4703
4704            for account in view["stat"]["accounts"].keys():
4705                info.extend([
4706                    "### ID: [{}]\n\n".format(account),
4707                    "| Parameters           | Values                                                       |\n",
4708                    "|----------------------|--------------------------------------------------------------|\n",
4709                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4710                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4711                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4712                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4713                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4714                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4715                ])
4716
4717                if margins[account]:
4718                    info.extend([
4719                        "| Margin status:       | Enabled                                                      |\n",
4720                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4721                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4722                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4723                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4724                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4725                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4726                    ])
4727
4728                else:
4729                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4730
4731            info.extend([
4732                "\n## Current user tariff limits\n",
4733                "\n### See also\n",
4734                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4735                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4736                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4737                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4738                "\n### Unary limits\n",
4739            ])
4740
4741            if unary:
4742                for key, values in sorted(unary.items()):
4743                    info.append("\n* Max requests per minute: {}\n".format(key))
4744
4745                    for value in values:
4746                        info.append("  - {}\n".format(value))
4747
4748            else:
4749                info.append("\nNot available\n")
4750
4751            info.append("\n### Stream limits\n")
4752
4753            if stream:
4754                for key, values in sorted(stream.items()):
4755                    info.append("\n* Max stream connections: {}\n".format(key))
4756
4757                    for value in values:
4758                        info.append("  - {}\n".format(value))
4759
4760            else:
4761                info.append("\nNot available\n")
4762
4763            infoText = "".join(info)
4764
4765            if show and not onlyFiles:
4766                uLogger.info(infoText)
4767
4768            if self.userInfoFile and (show or onlyFiles):
4769                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4770                    fH.write(infoText)
4771
4772                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4773
4774                if self.useHTMLReports:
4775                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4776                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4777                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4778
4779                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4780
4781        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4784class Args:
4785    """
4786    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4787    """
4788    def __init__(self, **kwargs):
4789        self.__dict__.update(kwargs)
4790
4791    def __getattr__(self, item):
4792        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4788    def __init__(self, **kwargs):
4789        self.__dict__.update(kwargs)
def ParseArgs():
4795def ParseArgs():
4796    """This function get and parse command line keys."""
4797    parser = ArgumentParser()  # command-line string parser
4798
4799    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4800    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4801
4802    # --- options:
4803
4804    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4805    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4806    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4807
4808    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4809    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4810
4811    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4812    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4813
4814    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4815    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4816
4817    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4818    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4819    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4820
4821    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4822    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4823    parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).")
4824
4825    # --- commands:
4826
4827    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4828
4829    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4830    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4831    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4832    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4833    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4834    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4835    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4836    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4837
4838    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4839    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4840    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4841    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4842    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4843    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4844
4845    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4846    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4847    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4848    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4849
4850    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4851    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4852    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4853
4854    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4855    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4856    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4857    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4858    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4859    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4860    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4861
4862    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4863    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4864    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4865    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4866    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4867
4868    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4869    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4870    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4871
4872    cmdArgs = parser.parse_args()
4873    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs):
4876def Main(**kwargs):
4877    """
4878    Main function for work with TKSBrokerAPI in the console.
4879
4880    See examples:
4881    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4882    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4883    """
4884    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4885
4886    if args.debug_level:
4887        uLogger.level = 10  # always debug level by default
4888        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4889
4890    exitCode = 0
4891    start = datetime.now(tzutc())
4892    uLogger.debug("=-" * 50)
4893    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4894        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4895        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4896    ))
4897
4898    # trying to calculate full current version:
4899    buildVersion = __version__
4900    try:
4901        v = version("tksbrokerapi")
4902        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4903
4904    except Exception:
4905        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4906
4907    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4908    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4909
4910    try:
4911        if args.version:
4912            print("TKSBrokerAPI {}".format(buildVersion))
4913            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4914
4915        else:
4916            # Init class for trading with Tinkoff Broker:
4917            trader = TinkoffBrokerServer(
4918                token=args.token,
4919                accountId=args.account_id,
4920                useCache=not args.no_cache,
4921            )
4922
4923            if args.tag is not None:
4924                trader.tag = args.tag  # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode
4925
4926            # --- set some options:
4927
4928            if args.more:
4929                trader.moreDebug = True
4930                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4931
4932            if args.html:
4933                trader.useHTMLReports = True
4934
4935            if args.ticker:
4936                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4937
4938                if ticker in trader.aliasesKeys:
4939                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4940
4941                else:
4942                    trader.ticker = ticker
4943
4944            if args.figi:
4945                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4946
4947            if args.depth is not None:
4948                trader.depth = args.depth
4949
4950            # --- do one command:
4951
4952            if args.list:
4953                if args.output is not None:
4954                    trader.instrumentsFile = args.output
4955
4956                trader.ShowInstrumentsInfo(show=True)
4957
4958            elif args.list_xlsx:
4959                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4960
4961            elif args.bonds_xlsx is not None:
4962                if args.output is not None:
4963                    trader.bondsXLSXFile = args.output
4964
4965                if len(args.bonds_xlsx) == 0:
4966                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4967
4968                else:
4969                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4970
4971            elif args.search:
4972                if args.output is not None:
4973                    trader.searchResultsFile = args.output
4974
4975                trader.SearchInstruments(pattern=args.search[0], show=True)
4976
4977            elif args.info:
4978                if not (args.ticker or args.figi):
4979                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4980                    raise Exception("Ticker or FIGI required")
4981
4982                if args.output is not None:
4983                    trader.infoFile = args.output
4984
4985                if args.ticker:
4986                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4987
4988                else:
4989                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4990
4991            elif args.calendar is not None:
4992                if args.output is not None:
4993                    trader.calendarFile = args.output
4994
4995                if len(args.calendar) == 0:
4996                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4997
4998                else:
4999                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
5000
5001                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
5002
5003            elif args.price:
5004                if not (args.ticker or args.figi):
5005                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5006                    raise Exception("Ticker or FIGI required")
5007
5008                trader.GetCurrentPrices(show=True)
5009
5010            elif args.prices is not None:
5011                if args.output is not None:
5012                    trader.pricesFile = args.output
5013
5014                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
5015
5016            elif args.overview:
5017                if args.output is not None:
5018                    trader.overviewFile = args.output
5019
5020                trader.Overview(show=True, details="full")
5021
5022            elif args.overview_digest:
5023                if args.output is not None:
5024                    trader.overviewDigestFile = args.output
5025
5026                trader.Overview(show=True, details="digest")
5027
5028            elif args.overview_positions:
5029                if args.output is not None:
5030                    trader.overviewPositionsFile = args.output
5031
5032                trader.Overview(show=True, details="positions")
5033
5034            elif args.overview_orders:
5035                if args.output is not None:
5036                    trader.overviewOrdersFile = args.output
5037
5038                trader.Overview(show=True, details="orders")
5039
5040            elif args.overview_analytics:
5041                if args.output is not None:
5042                    trader.overviewAnalyticsFile = args.output
5043
5044                trader.Overview(show=True, details="analytics")
5045
5046            elif args.overview_calendar:
5047                if args.output is not None:
5048                    trader.overviewAnalyticsFile = args.output
5049
5050                trader.Overview(show=True, details="calendar")
5051
5052            elif args.deals is not None:
5053                if args.output is not None:
5054                    trader.reportFile = args.output
5055
5056                if 0 <= len(args.deals) < 3:
5057                    trader.Deals(
5058                        start=args.deals[0] if len(args.deals) >= 1 else None,
5059                        end=args.deals[1] if len(args.deals) == 2 else None,
5060                        show=True,  # Always show deals report in console
5061                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5062                    )
5063
5064                else:
5065                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5066                    raise Exception("Incorrect value")
5067
5068            elif args.history is not None:
5069                if args.output is not None:
5070                    trader.historyFile = args.output
5071
5072                if 0 <= len(args.history) < 3:
5073                    dataReceived = trader.History(
5074                        start=args.history[0] if len(args.history) >= 1 else None,
5075                        end=args.history[1] if len(args.history) == 2 else None,
5076                        interval="hour" if args.interval is None or not args.interval else args.interval,
5077                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5078                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5079                        show=True,  # shows all downloaded candles in console
5080                    )
5081
5082                    if args.render_chart is not None and dataReceived is not None:
5083                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5084
5085                        trader.ShowHistoryChart(
5086                            candles=dataReceived,
5087                            interact=iChart,
5088                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5089                        )
5090
5091                else:
5092                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5093                    raise Exception("Incorrect value")
5094
5095            elif args.load_history is not None:
5096                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5097
5098                if args.render_chart is not None and histData is not None:
5099                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5100                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5101
5102                    trader.ShowHistoryChart(
5103                        candles=histData,
5104                        interact=iChart,
5105                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5106                    )
5107
5108            elif args.trade is not None:
5109                if 1 <= len(args.trade) <= 5:
5110                    trader.Trade(
5111                        operation=args.trade[0],
5112                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5113                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5114                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5115                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5116                    )
5117
5118                else:
5119                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5120
5121            elif args.buy is not None:
5122                if 0 <= len(args.buy) <= 4:
5123                    trader.Buy(
5124                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5125                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5126                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5127                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5128                    )
5129
5130                else:
5131                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5132
5133            elif args.sell is not None:
5134                if 0 <= len(args.sell) <= 4:
5135                    trader.Sell(
5136                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5137                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5138                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5139                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5140                    )
5141
5142                else:
5143                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5144
5145            elif args.order:
5146                if 4 <= len(args.order) <= 7:
5147                    trader.Order(
5148                        operation=args.order[0],
5149                        orderType=args.order[1],
5150                        lots=int(args.order[2]),
5151                        targetPrice=float(args.order[3]),
5152                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5153                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5154                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5155                    )
5156
5157                else:
5158                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5159
5160            elif args.buy_limit:
5161                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5162
5163            elif args.sell_limit:
5164                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5165
5166            elif args.buy_stop:
5167                if 2 <= len(args.buy_stop) <= 7:
5168                    trader.BuyStop(
5169                        lots=int(args.buy_stop[0]),
5170                        targetPrice=float(args.buy_stop[1]),
5171                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5172                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5173                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5174                    )
5175
5176                else:
5177                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5178
5179            elif args.sell_stop:
5180                if 2 <= len(args.sell_stop) <= 7:
5181                    trader.SellStop(
5182                        lots=int(args.sell_stop[0]),
5183                        targetPrice=float(args.sell_stop[1]),
5184                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5185                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5186                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5187                    )
5188
5189                else:
5190                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5191
5192            # elif args.buy_order_grid is not None:
5193            #     # update order grid work with api v2
5194            #     if len(args.buy_order_grid) == 2:
5195            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5196            #
5197            #         for order in orderParams:
5198            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5199            #
5200            #     else:
5201            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5202            #
5203            # elif args.sell_order_grid is not None:
5204            #     # update order grid work with api v2
5205            #     if len(args.sell_order_grid) >= 2:
5206            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5207            #
5208            #         for order in orderParams:
5209            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5210            #
5211            #     else:
5212            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5213
5214            elif args.close_order is not None:
5215                trader.CloseOrders(args.close_order)  # close only one order
5216
5217            elif args.close_orders is not None:
5218                trader.CloseOrders(args.close_orders)  # close list of orders
5219
5220            elif args.close_trade:
5221                if not (args.ticker or args.figi):
5222                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5223                    raise Exception("Ticker or FIGI required")
5224
5225                if args.ticker:
5226                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5227
5228                else:
5229                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5230
5231            elif args.close_trades is not None:
5232                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5233
5234            elif args.close_all is not None:
5235                if args.ticker:
5236                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5237
5238                elif args.figi:
5239                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5240
5241                else:
5242                    trader.CloseAll(*args.close_all)
5243
5244            elif args.limits:
5245                if args.output is not None:
5246                    trader.withdrawalLimitsFile = args.output
5247
5248                trader.OverviewLimits(show=True)
5249
5250            elif args.user_info:
5251                if args.output is not None:
5252                    trader.userInfoFile = args.output
5253
5254                trader.OverviewUserInfo(show=True)
5255
5256            elif args.account:
5257                if args.output is not None:
5258                    trader.userAccountsFile = args.output
5259
5260                trader.OverviewAccounts(show=True)
5261
5262            else:
5263                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5264                raise Exception("There is no command to execute")
5265
5266    except Exception:
5267        trace = tb.format_exc()
5268        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5269            if e in trace:
5270                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5271                break
5272
5273        uLogger.debug(trace)
5274        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5275        exitCode = 255  # an error occurred, must be open a ticket for this issue
5276
5277    finally:
5278        finish = datetime.now(tzutc())
5279
5280        if exitCode == 0:
5281            if args.more:
5282                uLogger.debug("All operations were finished success (summary code is 0).")
5283
5284        else:
5285            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5286                os.path.abspath(uLog.defaultLogFile), exitCode,
5287            ))
5288
5289        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5290        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5291            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5292            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5293        ))
5294        uLogger.debug("=-" * 50)
5295
5296        if not kwargs:
5297            sys.exit(exitCode)
5298
5299        else:
5300            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: